phy/utils: DFI rate converter for creating PHY wrappers at slower clocks

This commit is contained in:
Jędrzej Boczar 2021-07-23 14:59:55 +02:00
parent 46ea844702
commit 89af25a697
4 changed files with 643 additions and 8 deletions

View File

@ -2,11 +2,14 @@
# This file is part of LiteDRAM.
#
# Copyright (c) 2015 Sebastien Bourdeauducq <sb@m-labs.hk>
# Copyright (c) 2021 Antmicro <www.antmicro.com>
# 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)

View File

@ -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)

View File

@ -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

489
test/test_dfi.py Normal file
View File

@ -0,0 +1,489 @@
#
# This file is part of LiteDRAM.
#
# Copyright (c) 2021 Antmicro <www.antmicro.com>
# 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)