diff --git a/litedram/phy/dfi.py b/litedram/phy/dfi.py index 8bf9063..548e9d4 100644 --- a/litedram/phy/dfi.py +++ b/litedram/phy/dfi.py @@ -2,11 +2,14 @@ # This file is part of LiteDRAM. # # Copyright (c) 2015 Sebastien Bourdeauducq +# Copyright (c) 2021 Antmicro # SPDX-License-Identifier: BSD-2-Clause from migen import * from migen.genlib.record import * +from litedram.phy.utils import Serializer, Deserializer + def phase_cmd_description(addressbits, bankbits, nranks): return [ @@ -97,3 +100,130 @@ class DDR4DFIMux(Module): p_o.act_n.eq(1), ) ] + + +class DFIRateConverter(Module): + """Converts between DFI interfaces running at different clock frequencies + + This module allows to convert DFI interface `phy_dfi` running at higher clock frequency + into a DFI interface running at `ratio` lower frequency. The new DFI has `ratio` more + phases and the commands on the following phases of the new DFI will be serialized to + following phases/clocks of `phy_dfi` (phases first, then clock cycles). + + Data must be serialized/deserialized in such a way that a whole burst on `phy_dfi` is + sent in a single `clk` cycle. For this reason, the new DFI interface will have `ratio` + less databits. For example, with phy_dfi(nphases=2, databits=32) and ratio=4 the new + DFI will have nphases=8, databits=8. This results in 8*8=64 bits in `clkdiv` translating + into 2*32=64 bits in `clk`. This means that only a single cycle of `clk` per `clkdiv` + cycle carries the data (by default cycle 0). This can be modified by passing values + different than 0 for `write_delay`/`read_delay` and may be needed to properly align + write/read latency of the original PHY and the wrapper. + """ + def __init__(self, phy_dfi, *, clkdiv, clk, ratio, serdes_reset_cnt=-1, write_delay=0, read_delay=0): + assert len(phy_dfi.p0.wrdata) % ratio == 0 + assert 0 <= write_delay < ratio, f"Data can be delayed up to {ratio} clk cycles" + assert 0 <= read_delay < ratio, f"Data can be delayed up to {ratio} clk cycles" + + self.ser_latency = Serializer.LATENCY + self.des_latency = Deserializer.LATENCY + + phase_params = dict( + addressbits = len(phy_dfi.p0.address), + bankbits = len(phy_dfi.p0.bank), + nranks = len(phy_dfi.p0.cs_n), + databits = len(phy_dfi.p0.wrdata) // ratio, + ) + self.dfi = Interface(nphases=ratio * len(phy_dfi.phases), **phase_params) + + wr_delayed = ["wrdata", "wrdata_mask"] + rd_delayed = ["rddata", "rddata_valid"] + + for name, width, dir in phase_description(**phase_params): + # all signals except write/read + if name in wr_delayed + rd_delayed: + continue + # on each clk phase + for pi, phase_s in enumerate(phy_dfi.phases): + sig_s = getattr(phase_s, name) + assert len(sig_s) == width + + # data from each clkdiv phase + sigs_m = [] + for j in range(ratio): + phase_m = self.dfi.phases[pi + len(phy_dfi.phases)*j] + sigs_m.append(getattr(phase_m, name)) + + ser = Serializer( + clkdiv = clkdiv, + clk = clk, + i_dw = ratio*width, + o_dw = width, + i = Cat(sigs_m), + o = sig_s, + reset_cnt = serdes_reset_cnt, + name = name, + ) + self.submodules += ser + + # wrdata + for name, width, dir in phase_description(**phase_params): + if name not in wr_delayed: + continue + for pi, phase_s in enumerate(phy_dfi.phases): + sig_s = getattr(phase_s, name) + sig_m = Signal(len(sig_s) * ratio) + + sigs_m = [] + for j in range(ratio): + phase_m = self.dfi.phases[pi*ratio + j] + sigs_m.append(getattr(phase_m, name)) + + width = len(Cat(sigs_m)) + self.comb += sig_m[write_delay*width:(write_delay+1)*width].eq(Cat(sigs_m)) + + o = Signal.like(sig_s) + ser = Serializer( + clkdiv = clkdiv, + clk = clk, + i_dw = len(sig_m), + o_dw = len(sig_s), + i = sig_m, + o = o, + reset_cnt = serdes_reset_cnt, + name = name, + ) + self.submodules += ser + + self.comb += sig_s.eq(o) + + # rddata + for name, width, dir in phase_description(**phase_params): + if name not in rd_delayed: + continue + for pi, phase_s in enumerate(phy_dfi.phases): + sig_s = getattr(phase_s, name) + + sig_m = Signal(ratio * len(sig_s)) + sigs_m = [] + for j in range(ratio): + phase_m = self.dfi.phases[pi*ratio + j] + sigs_m.append(getattr(phase_m, name)) + + des = Deserializer( + clkdiv = clkdiv, + clk = clk, + i_dw = len(sig_s), + o_dw = len(sig_m), + i = sig_s, + o = sig_m, + reset_cnt = serdes_reset_cnt, + name = name, + ) + self.submodules += des + + if name == "rddata_valid": + self.comb += Cat(sigs_m).eq(Replicate(sig_m[read_delay], ratio)) + else: + out_width = len(Cat(sigs_m)) + sig_m_window = sig_m[read_delay*out_width:(read_delay + 1)*out_width] + self.comb += Cat(sigs_m).eq(sig_m_window) diff --git a/litedram/phy/utils.py b/litedram/phy/utils.py index b54a589..b7e2625 100644 --- a/litedram/phy/utils.py +++ b/litedram/phy/utils.py @@ -228,8 +228,8 @@ class Serializer(Module): LATENCY = 1 def __init__(self, clkdiv, clk, i_dw, o_dw, i=None, o=None, reset=None, reset_cnt=-1, name=None): - assert i_dw > o_dw - assert i_dw % o_dw == 0 + assert i_dw > o_dw, (i_dw, o_dw) + assert i_dw % o_dw == 0, (i_dw, o_dw) ratio = i_dw // o_dw sd_clk = getattr(self.sync, clk) @@ -271,8 +271,8 @@ class Deserializer(Module): LATENCY = 2 def __init__(self, clkdiv, clk, i_dw, o_dw, i=None, o=None, reset=None, reset_cnt=-1, name=None): - assert i_dw < o_dw - assert o_dw % i_dw == 0 + assert i_dw < o_dw, (i_dw, o_dw) + assert o_dw % i_dw == 0, (i_dw, o_dw) ratio = o_dw // i_dw sd_clk = getattr(self.sync, clk) diff --git a/test/phy_common.py b/test/phy_common.py index bb8d749..1995da0 100644 --- a/test/phy_common.py +++ b/test/phy_common.py @@ -179,6 +179,9 @@ def dfi_names(cmd=True, wrdata=True, rddata=True): if rddata: names += [name for name, _, _ in dfi.phase_rddata_description(16)] return names +def dfi_reset_values(**kwargs): + return {sig: 1 if sig.endswith("_n") else 0 for sig in dfi_names(**kwargs)} + class DFIPhaseValues(dict): """Dictionary {dfi_signal_name: value}""" @@ -219,12 +222,9 @@ class DFISequencer: def add(self, dfi_cycle: Mapping[DFIPhase, DFIPhaseValues]): self.sequence.append(dfi_cycle) - def _dfi_reset_values(self): - return {sig: 1 if sig.endswith("_n") else 0 for sig in dfi_names()} - def _reset(self, dfi): for phase in dfi.phases: - for sig, val in self._dfi_reset_values().items(): + for sig, val in dfi_reset_values().items(): yield getattr(phase, sig).eq(val) def assert_ok(self, test_case): @@ -268,3 +268,19 @@ class DFISequencer: phases[i] = values self.read_sequence.append(phases) yield + + @staticmethod + def input_generator(dfi, sequence: DFISequence): + names = dfi_names(cmd=True, wrdata=True, rddata=False) + ["rddata_en"] + for per_phase in sequence: + # set values + for phase, values in per_phase.items(): + for sig, val in values.items(): + assert sig not in names, f"`{sig}` is not DFI input signal" + yield getattr(dfi.phases[phase], sig).eq(val) + yield + # reset values + for phase, values in per_phase.items(): + for sig in values.keys(): + yield getattr(dfi.phases[phase], sig).eq(0) + yield diff --git a/test/test_dfi.py b/test/test_dfi.py new file mode 100644 index 0000000..faa2575 --- /dev/null +++ b/test/test_dfi.py @@ -0,0 +1,489 @@ +# +# This file is part of LiteDRAM. +# +# Copyright (c) 2021 Antmicro +# SPDX-License-Identifier: BSD-2-Clause + +import os +import unittest +from collections import defaultdict + +from migen import * + +from litex.build.sim import gtkwave as gtkw + +from litedram.phy.dfi import Interface as DFIInterface, DFIRateConverter +from litedram.phy.utils import Serializer, Deserializer + +from test.phy_common import DFISequencer, dfi_reset_values, run_simulation as _run_simulation + + +def run_simulation(ratio, *args, **kwargs): + clkdiv = "sys" + clk = f"sys{ratio}x" + kwargs["clocks"] = { + clkdiv: (4*ratio, 4*ratio/2 - 1), + clk: (4, 1), + } + _run_simulation(*args, **kwargs) + + +# To debug the tests pass vcd_name="sim.vcd" to the self.run_test call and use `gtkwave sim.gtkw` to view the trace +class TestPHYRateConverter(unittest.TestCase): + class Dut(Module): + def __init__(self, ratio, converter_kwargs=None, **dfi_kwargs): + self.ratio = ratio + self.dfi_old = DFIInterface(**dfi_kwargs) + converter_kwargs = converter_kwargs or {} + self.submodules.converter = DFIRateConverter(self.dfi_old, clkdiv="sys", clk=f"sys{ratio}x", + ratio=ratio, serdes_reset_cnt=-1, **converter_kwargs) + self.dfi = self.converter.dfi + + def dfi_latency(self, ratio, reset_n=True): + latency_clkdiv = Serializer.LATENCY + latency_clk = ratio * latency_clkdiv + nop = {p: (dict(reset_n=0) if reset_n else {}) for p in range(4)} # reset_n will have wrong value until driven + return [nop] * latency_clk + + def run_test(self, dut, dfi_sys, dfi_expected, dfi_input=None, **kwargs): + assert callable(dut), "dut must be a callable that returns new Dut instance" + dfi = DFISequencer(dfi_sys) + + # Add GTKWave savefile for debugging if we are creating a VCD dumpfile (requires pyvcd installed) + if kwargs.get("vcd_name", False): + dumpfile = kwargs["vcd_name"] + savefile = os.path.splitext(dumpfile)[0] + ".gtkw" + # Create a separate dut just for the purpose of generaing a savefile + tmp_dut = dut() + vns = verilog.convert(tmp_dut).ns + + with gtkw.GTKWSave(vns, savefile=savefile, dumpfile=dumpfile, prefix="") as save: + save.clocks() + for grp_dfi, grp_name in [(tmp_dut.dfi, "dfi new"), (tmp_dut.dfi_old, "dfi old")]: + with save.gtkw.group(grp_name): + # each phase in separate group + with save.gtkw.group("dfi phaseX", closed=True): + for i, phase in enumerate(grp_dfi.phases): + save.add(phase, group_name="dfi p{}".format(i), mappers=[ + gtkw.dfi_sorter(phases=False), + gtkw.dfi_in_phase_colorer(), + ]) + # only dfi command signals + save.add(grp_dfi, group_name="dfi commands", mappers=[ + gtkw.regex_filter(gtkw.suffixes2re(["cas_n", "ras_n", "we_n"])), + gtkw.dfi_sorter(), + gtkw.dfi_per_phase_colorer(), + ]) + # only dfi data signals + save.add(grp_dfi, group_name="dfi wrdata", mappers=[ + gtkw.regex_filter(["wrdata$"]), + gtkw.dfi_sorter(), + gtkw.dfi_per_phase_colorer(), + ]) + save.add(grp_dfi, group_name="dfi wrdata_mask", mappers=[ + gtkw.regex_filter(gtkw.suffixes2re(["wrdata_mask"])), + gtkw.dfi_sorter(), + gtkw.dfi_per_phase_colorer(), + ]) + save.add(grp_dfi, group_name="dfi rddata", mappers=[ + gtkw.regex_filter(gtkw.suffixes2re(["rddata", "p0.*rddata_valid"])), + gtkw.dfi_sorter(), + gtkw.dfi_per_phase_colorer(), + ]) + + def checker(dfi): + fail = False + + history = defaultdict(list) + reference = defaultdict(list) + + # first cycle has undefined data until DFISequencer drives the signals + yield + + for i, dfi_phases in enumerate(dfi_expected): + for phase in range(len(dfi.phases)): + values = dfi_reset_values() + values.update(dfi_phases.get(phase, {})) + for name, ref in values.items(): + # if name in ["rddata", "rddata_valid"]: + # continue + val = (yield getattr(dfi.phases[phase], name)) + msg = f"Cycle {i}, dfi.p{phase}.{name} = {val} != {ref}" + history[name].append(val) + reference[name].append(ref) + if not fail and val != ref: + fail = (val, ref, msg) + yield + + def split_cycles(hist): + s = "" + for i, val in enumerate(hist): + s += str(val) + if (i + 1) % len(dfi.phases) == 0: + s += " " + return s + + if fail: + print() + for sig in history: + if len(getattr(dfi.phases[0], sig)) == 1: + print(f"{sig:12}: {split_cycles(history[sig])}") + print(" "*14 + f"{split_cycles(reference[sig])}") + self.assertEqual(*fail) + + dut = dut() + run_simulation(dut.ratio, dut, generators={ + "sys": [dfi.generator(dut.dfi), dfi.reader(dut.dfi)], + f"sys{dut.ratio}x": [checker(dut.dfi_old), DFISequencer.input_generator(dut.dfi_old, dfi_input or [])], + }, **kwargs) + dfi.assert_ok(self) + + def test_dfi_rate_converter_phase_0(self): + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + dfi_sys = [ + {0: read}, + ] + dfi_expected = [ + *self.dfi_latency(ratio=2), + {0: read}, + {}, + ] + self.run_test(lambda: self.Dut( + ratio=2, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=4, + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_2_cmd(self): + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + activate = dict(cs_n=0, cas_n=1, ras_n=0, we_n=1, bank=0b010, address=0b1110000111100001) + precharge = dict(cs_n=0, cas_n=1, ras_n=0, we_n=0, bank=0b111, address=0) + dfi_sys = [ + {1: activate, 3: read, 6: precharge}, + ] + dfi_expected = [ + *self.dfi_latency(ratio=2), + {1: activate, 3: read}, + {2: precharge}, + {}, {}, # 1 + ] + self.run_test(lambda: self.Dut( + ratio=2, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=4, + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_2_write(self): + write_ap = dict(cs_n=0, cas_n=0, ras_n=1, we_n=0, bank=0b111, address=0b10000000000, wrdata_en=1) + data_clkdiv = { + 0: dict(wrdata=0x1111, wrdata_mask=0b00), + 1: dict(wrdata=0x2222, wrdata_mask=0b11), + 2: dict(wrdata=0x3333, wrdata_mask=0b00), + 3: dict(wrdata=0x4444, wrdata_mask=0b11), + 4: dict(wrdata=0x5555, wrdata_mask=0b01), + 5: dict(wrdata=0x6666, wrdata_mask=0b01), + 6: dict(wrdata=0x7777, wrdata_mask=0b10), + 7: dict(wrdata=0x8888, wrdata_mask=0b01), + } + data_clk = { + 0: dict(wrdata=0x22221111, wrdata_mask=0b1100), + 1: dict(wrdata=0x44443333, wrdata_mask=0b1100), + 2: dict(wrdata=0x66665555, wrdata_mask=0b0101), + 3: dict(wrdata=0x88887777, wrdata_mask=0b0110), + } + dfi_sys = [ + {7: write_ap}, + {}, + data_clkdiv, + ] + dfi_expected = [ + *self.dfi_latency(ratio=2), + {}, # 0 + {3: write_ap}, + {}, # 1 + {}, + data_clk, # 2 (assuming write latency = 3) + {}, + {}, # 3 + {}, + ] + self.run_test(lambda: self.Dut( + ratio=2, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=4, + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_2_read(self): + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + data_clkdiv = { + 0: dict(rddata=0x1111, rddata_valid=1), + 1: dict(rddata=0x2222), + 2: dict(rddata=0x3333), + 3: dict(rddata=0x4444), + 4: dict(rddata=0x5555), + 5: dict(rddata=0x6666), + 6: dict(rddata=0x7777), + 7: dict(rddata=0x8888), + } + data_clk = { + 0: dict(rddata=0x22221111, rddata_valid=1), + 1: dict(rddata=0x44443333), + 2: dict(rddata=0x66665555), + 3: dict(rddata=0x88887777), + } + des_latency = [{}] * Deserializer.LATENCY + dfi_sys = [ + {7: read}, # 0 + {}, + {}, # 2 (data_clk) + *des_latency, + data_clkdiv, + ] + dfi_expected = [ # sys2x + *self.dfi_latency(ratio=2), + {}, # 0 + {3: read}, + ] + dfi_input = [ # sys2x + *self.dfi_latency(ratio=2, reset_n=False), + {}, # 0 + {}, # read + {}, # 1 + {}, + data_clk, # 2 (assumig read latency = 3) + ] + self.run_test(lambda: self.Dut( + ratio=2, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=4, + ), dfi_sys, dfi_expected, dfi_input=dfi_input) + + def test_dfi_rate_converter_1_to_4_cmd(self): + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + activate = dict(cs_n=0, cas_n=1, ras_n=0, we_n=1, bank=0b010, address=0b1110000111100001) + precharge = dict(cs_n=0, cas_n=1, ras_n=0, we_n=0, bank=0b111, address=0) + dfi_sys = [ + {1: activate, 3: read, 6: precharge}, + ] + dfi_expected = [ + *self.dfi_latency(ratio=4), + {1: activate}, # 0 + {1: read}, + {}, + {0: precharge}, + {}, {}, {}, {}, # 1 + ] + self.run_test(lambda: self.Dut( + ratio=4, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=2, + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_4_write(self): + write_ap = dict(cs_n=0, cas_n=0, ras_n=1, we_n=0, bank=0b111, address=0b10000000000, wrdata_en=1) + data_clkdiv = { + 0: dict(wrdata=0x11), + 1: dict(wrdata=0x22), + 2: dict(wrdata=0x33), + 3: dict(wrdata=0x44), + 4: dict(wrdata=0x55), + 5: dict(wrdata=0x66), + 6: dict(wrdata=0x77), + 7: dict(wrdata=0x88), + } + data_clk = { + 0: dict(wrdata=0x44332211), + 1: dict(wrdata=0x88776655), + } + dfi_sys = [ + {7: write_ap}, + {}, + data_clkdiv, + ] + dfi_expected = [ + *self.dfi_latency(ratio=4), + {}, # 0 + {}, + {}, + {1: write_ap}, + {}, {}, {}, {}, # 1 + data_clk, # 2 (assuming write latency = 5) + {}, {}, {}, + {}, {}, {}, {}, # 3 + ] + self.run_test(lambda: self.Dut( + ratio=4, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=2, + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_4_read(self): + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + data_clkdiv = { + 0: dict(rddata=0x1111, rddata_valid=1), + 1: dict(rddata=0x2222, rddata_valid=1), + 2: dict(rddata=0x3333, rddata_valid=1), + 3: dict(rddata=0x4444, rddata_valid=1), + 4: dict(rddata=0x5555, rddata_valid=1), + 5: dict(rddata=0x6666, rddata_valid=1), + 6: dict(rddata=0x7777, rddata_valid=1), + 7: dict(rddata=0x8888, rddata_valid=1), + } + data_clk = { + 0: dict(rddata=0x4444333322221111, rddata_valid=1), + 1: dict(rddata=0x8888777766665555, rddata_valid=1), + } + des_latency = [{}] * Deserializer.LATENCY + dfi_sys = [ + {7: read}, # 0 + {}, + {}, # 2 (data_clk) + *des_latency, + data_clkdiv, + ] + dfi_expected = [ # sys2x + *self.dfi_latency(ratio=4), + {}, # 0 + {}, + {}, + {1: read}, + ] + dfi_input = [ # sys2x + *self.dfi_latency(ratio=4, reset_n=False), + {}, # 0 + {}, + {}, + {}, # read + {}, {}, {}, {}, # 1 + data_clk, # 2 (assumig read latency = 5) + ] + self.run_test(lambda: self.Dut( + ratio=4, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*32, + nphases=2, + ), dfi_sys, dfi_expected, dfi_input=dfi_input) + + def test_dfi_rate_converter_1_to_4_write_delayed(self): + # When write_latency does not aligh with clkdiv boundaries, the write data must be delayed + write_ap = dict(cs_n=0, cas_n=0, ras_n=1, we_n=0, bank=0b111, address=0b10000000000, wrdata_en=1) + data_clkdiv = { + 0: dict(wrdata=0x11), + 1: dict(wrdata=0x22), + 2: dict(wrdata=0x33), + 3: dict(wrdata=0x44), + 4: dict(wrdata=0x55), + 5: dict(wrdata=0x66), + 6: dict(wrdata=0x77), + 7: dict(wrdata=0x88), + } + data_clk = { + 0: dict(wrdata=0x44332211), + 1: dict(wrdata=0x88776655), + } + for write_latency, sys_latency, write_delay in [(1, 0, 0), (2, 0, 1), (3, 0, 2), (4, 0, 3), (5, 1, 0)]: + with self.subTest(write_latency=write_latency, sys_latency=sys_latency, write_delay=write_delay): + sys_latency = [{}] * sys_latency + write_latency_cycles = [{}] * (write_latency - 1) + dfi_sys = [ + {7: write_ap}, + *sys_latency, + data_clkdiv, # send with sys write_latency=1 + ] + dfi_expected = [ + *self.dfi_latency(ratio=4), + {}, # 0 + {}, + {}, + {1: write_ap}, + # 1 + *write_latency_cycles, + data_clk, + {}, {}, {}, {}, + ] + self.run_test(lambda: self.Dut( + ratio=4, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*16, + nphases=2, + converter_kwargs=dict(write_delay=write_delay), + ), dfi_sys, dfi_expected) + + def test_dfi_rate_converter_1_to_4_read_delayed(self): + # When read_latency does not aligh with clkdiv boundaries, the read data must be delayed + read = dict(cs_n=0, cas_n=0, ras_n=1, we_n=1, bank=0b111, address=0b110101) + data_clkdiv = { + 0: dict(rddata=0x1111, rddata_valid=1), # make sure it rddata_valid on 1 phase is replicated to ratio phases + 1: dict(rddata=0x2222, rddata_valid=1), + 2: dict(rddata=0x3333, rddata_valid=1), + 3: dict(rddata=0x4444, rddata_valid=1), + 4: dict(rddata=0x5555), + 5: dict(rddata=0x6666), + 6: dict(rddata=0x7777), + 7: dict(rddata=0x8888), + } + data_clk = { + 0: dict(rddata=0x4444333322221111, rddata_valid=1), + 1: dict(rddata=0x8888777766665555), + } + for read_latency, sys_latency, read_delay in [(1, 0, 0), (2, 0, 1), (3, 0, 2), (4, 0, 3), (5, 1, 0)]: + # read_latency is the PHY's read latency + # sys_latency is additional latency added at sys clock + # read_delay is from which cycle at sys4x the data is taken + with self.subTest(read_latency=read_latency, sys_latency=sys_latency, read_delay=read_delay): + sys_latency = [{}] * (Deserializer.LATENCY + sys_latency) + read_latency_cycles = [{}] * (read_latency - 1) + dfi_sys = [ + {7: read}, # 0 + {}, # 1 (read command shows up on dfi_old) + *sys_latency, + data_clkdiv, + ] + dfi_expected = [ # sys2x + *self.dfi_latency(ratio=4), + {}, # 0 + {}, + {}, + {1: read}, + ] + dfi_input = [ # sys2x + *self.dfi_latency(ratio=4, reset_n=False), + {}, # 0 + {}, + {}, + {}, # read + # 1 + *read_latency_cycles, + data_clk, + ] + self.run_test(lambda: self.Dut( + ratio=4, + addressbits=16, + bankbits=3, + nranks=1, + databits=2*32, + nphases=2, + converter_kwargs=dict(read_delay=read_delay), + ), dfi_sys, dfi_expected, dfi_input=dfi_input)