diff --git a/litedram/phy/lpddr4/simphy.py b/litedram/phy/lpddr4/simphy.py index afbddae..36cd15b 100644 --- a/litedram/phy/lpddr4/simphy.py +++ b/litedram/phy/lpddr4/simphy.py @@ -6,7 +6,7 @@ from migen import * -from litedram.phy.utils import delayed, Serializer, Deserializer, Latency, SimPad, SimulationPads +from litedram.phy.utils import delayed, Serializer, Deserializer, Latency, SimPad, SimulationPads, SimSerDesMixin from litedram.phy.lpddr4.basephy import LPDDR4PHY, DoubleRateLPDDR4PHY @@ -25,84 +25,11 @@ class LPDDR4SimulationPads(SimulationPads): ] -class _LPDDR4SimPHYMixin: - """Common serialization logic for simulation PHYs +class LPDDR4SimPHY(SimSerDesMixin, LPDDR4PHY): + """LPDDR4 simulation PHY with direct 16:1 serializers - This mixin provides `do_serialization` method for constructing the boilerplate - serialization/deserialization paths for a simulation PHY. This can serve as a - reference for implemeing PHYs for concrete FPGAs. - - To make the (de-)serialization work in simulation two additional clock domains - are required: `sys8x_ddr` and `sys8x_90_ddr`. These correspond to `sys8x` and - `sys8x_90`, are phase aligned with them and at twice their frequency. These - clock domains are requried to implement DDR (de-)serialization at 8x sys clock. + For simulation purpose two additional "DDR" clock domains are requires. """ - def _add_name(self, prefix, kwargs): - name = prefix + "_" + kwargs.pop("name", "") - kwargs["name"] = name.strip("_") - - def _serialize(self, **kwargs): - self._add_name("ser", kwargs) - ser = Serializer(o_dw=1, **kwargs) - self.submodules += ser - - def _deserialize(self, **kwargs): - self._add_name("des", kwargs) - des = Deserializer(i_dw=1, **kwargs) - self.submodules += des - - def do_serialization(self, clkdiv, delay, aligned_reset_zero): - def add_reset_cnt(phase, kwargs): - if aligned_reset_zero and phase == 0: - kwargs["reset_cnt"] = 0 - - def ser_sdr(phase=0, **kwargs): - add_reset_cnt(phase, kwargs) - clk = {0: "sys8x", 90: "sys8x_90"}[phase] - self._serialize(clk=clk, clkdiv=clkdiv, i_dw=len(kwargs["i"]), **kwargs) - - def ser_ddr(phase=0, **kwargs): - add_reset_cnt(phase, kwargs) - # for simulation we require sys8x_ddr clock (=sys16x) - clk = {0: "sys8x_ddr", 90: "sys8x_90_ddr"}[phase] - self._serialize(clk=clk, clkdiv=clkdiv, i_dw=len(kwargs["i"]), **kwargs) - - def des_ddr(phase=0, **kwargs): - add_reset_cnt(phase, kwargs) - clk = {0: "sys8x_ddr", 90: "sys8x_90_ddr"}[phase] - self._deserialize(clk=clk, clkdiv=clkdiv, o_dw=len(kwargs["o"]), **kwargs) - - # Clock is shifted 180 degrees to get rising edge in the middle of SDR signals. - # To achieve that we send negated clock on clk (clk_p). - ser_ddr(i=~self.out.clk, o=self.pads.clk, name='clk') - - ser_sdr(i=self.out.cke, o=self.pads.cke, name='cke') - ser_sdr(i=self.out.odt, o=self.pads.odt, name='odt') - ser_sdr(i=self.out.reset_n, o=self.pads.reset_n, name='reset_n') - - # Command/address - ser_sdr(i=self.out.cs, o=self.pads.cs, name='cs') - for i in range(6): - ser_sdr(i=self.out.ca[i], o=self.pads.ca[i], name=f'ca{i}') - - # Tristate I/O (separate for simulation) - for i in range(self.databits//8): - ser_ddr(i=self.out.dmi_o[i], o=self.pads.dmi_o[i], name=f'dmi_o{i}') - des_ddr(o=self.out.dmi_i[i], i=self.pads.dmi[i], name=f'dmi_i{i}') - ser_ddr(i=self.out.dqs_o[i], o=self.pads.dqs_o[i], name=f'dqs_o{i}', phase=90) - des_ddr(o=self.out.dqs_i[i], i=self.pads.dqs[i], name=f'dqs_i{i}', phase=90) - for i in range(self.databits): - ser_ddr(i=self.out.dq_o[i], o=self.pads.dq_o[i], name=f'dq_o{i}') - des_ddr(o=self.out.dq_i[i], i=self.pads.dq[i], name=f'dq_i{i}') - - # Output enable signals - self.comb += self.pads.dmi_oe.eq(delay(self.out.dmi_oe, cycles=Serializer.LATENCY)) - self.comb += self.pads.dqs_oe.eq(delay(self.out.dqs_oe, cycles=Serializer.LATENCY)) - self.comb += self.pads.dq_oe.eq(delay(self.out.dq_oe, cycles=Serializer.LATENCY)) - - -class LPDDR4SimPHY(_LPDDR4SimPHYMixin, LPDDR4PHY): - """LPDDR4 simulation PHY with direct 16:1 serializers""" def __init__(self, aligned_reset_zero=False, **kwargs): pads = LPDDR4SimulationPads() self.submodules += pads @@ -112,18 +39,54 @@ class LPDDR4SimPHY(_LPDDR4SimPHYMixin, LPDDR4PHY): phytype = "LPDDR4SimPHY", **kwargs) - self.do_serialization( - clkdiv = "sys", - delay = lambda sig, cycles: delayed(self, sig, cycles=cycles), - aligned_reset_zero = aligned_reset_zero, - ) + delay = lambda sig, cycles: delayed(self, sig, cycles=cycles) + sdr = dict(clkdiv="sys", clk="sys8x") + sdr_90 = dict(clkdiv="sys", clk="sys8x_90") + ddr = dict(clkdiv="sys", clk="sys8x_ddr") + ddr_90 = dict(clkdiv="sys", clk="sys8x_90_ddr") + + if aligned_reset_zero: + sdr["reset_cnt"] = 0 + ddr["reset_cnt"] = 0 + + # Clock is shifted 180 degrees to get rising edge in the middle of SDR signals. + # To achieve that we send negated clock on clk (clk_p). + self.ser(i=~self.out.clk, o=self.pads.clk, name='clk', **ddr) + + self.ser(i=self.out.cke, o=self.pads.cke, name='cke', **sdr) + self.ser(i=self.out.odt, o=self.pads.odt, name='odt', **sdr) + self.ser(i=self.out.reset_n, o=self.pads.reset_n, name='reset_n', **sdr) + + # Command/address + self.ser(i=self.out.cs, o=self.pads.cs, name='cs', **sdr) + for i in range(6): + self.ser(i=self.out.ca[i], o=self.pads.ca[i], name=f'ca{i}', **sdr) + + # Tristate I/O (separate for simulation) + for i in range(self.databits//8): + self.ser(i=self.out.dmi_o[i], o=self.pads.dmi_o[i], name=f'dmi_o{i}', **ddr) + self.des(o=self.out.dmi_i[i], i=self.pads.dmi[i], name=f'dmi_i{i}', **ddr) + self.ser(i=self.out.dqs_o[i], o=self.pads.dqs_o[i], name=f'dqs_o{i}', **ddr_90) + self.des(o=self.out.dqs_i[i], i=self.pads.dqs[i], name=f'dqs_i{i}', **ddr_90) + for i in range(self.databits): + self.ser(i=self.out.dq_o[i], o=self.pads.dq_o[i], name=f'dq_o{i}', **ddr) + self.des(o=self.out.dq_i[i], i=self.pads.dq[i], name=f'dq_i{i}', **ddr) + + # Output enable signals + self.comb += [ + self.pads.dmi_oe.eq(delay(self.out.dmi_oe, cycles=Serializer.LATENCY)), + self.pads.dqs_oe.eq(delay(self.out.dqs_oe, cycles=Serializer.LATENCY)), + self.pads.dq_oe.eq(delay(self.out.dq_oe, cycles=Serializer.LATENCY)), + ] -class DoubleRateLPDDR4SimPHY(_LPDDR4SimPHYMixin, DoubleRateLPDDR4PHY): +class DoubleRateLPDDR4SimPHY(SimSerDesMixin, DoubleRateLPDDR4PHY): """LPDDR4 simulation PHY basing of DoubleRateLPDDR4PHY `DoubleRateLPDDR4PHY` performs a single serialization step between `sys` and `sys2x`, so this PHY wrapper has to do the serialization between `sys2x` and `sys8x` (SDR/DDR). + + For simulation purpose two additional "DDR" clock domains are requires. """ def __init__(self, aligned_reset_zero=False, **kwargs): pads = LPDDR4SimulationPads() @@ -135,9 +98,43 @@ class DoubleRateLPDDR4SimPHY(_LPDDR4SimPHYMixin, DoubleRateLPDDR4PHY): **kwargs) self.submodules.half_delay = ClockDomainsRenamer("sys2x")(Module()) + delay = lambda sig, cycles: delayed(self.half_delay, sig, cycles=cycles) - self.do_serialization( - clkdiv = "sys2x", - delay = lambda sig, cycles: delayed(self.half_delay, sig, cycles=cycles), - aligned_reset_zero = aligned_reset_zero, - ) + sdr = dict(clkdiv="sys2x", clk="sys8x") + sdr_90 = dict(clkdiv="sys2x", clk="sys8x_90") + ddr = dict(clkdiv="sys2x", clk="sys8x_ddr") + ddr_90 = dict(clkdiv="sys2x", clk="sys8x_90_ddr") + + if aligned_reset_zero: + sdr["reset_cnt"] = 0 + ddr["reset_cnt"] = 0 + + # Clock is shifted 180 degrees to get rising edge in the middle of SDR signals. + # To achieve that we send negated clock on clk (clk_p). + self.ser(i=~self.out.clk, o=self.pads.clk, name='clk', **ddr) + + self.ser(i=self.out.cke, o=self.pads.cke, name='cke', **sdr) + self.ser(i=self.out.odt, o=self.pads.odt, name='odt', **sdr) + self.ser(i=self.out.reset_n, o=self.pads.reset_n, name='reset_n', **sdr) + + # Command/address + self.ser(i=self.out.cs, o=self.pads.cs, name='cs', **sdr) + for i in range(6): + self.ser(i=self.out.ca[i], o=self.pads.ca[i], name=f'ca{i}', **sdr) + + # Tristate I/O (separate for simulation) + for i in range(self.databits//8): + self.ser(i=self.out.dmi_o[i], o=self.pads.dmi_o[i], name=f'dmi_o{i}', **ddr) + self.des(o=self.out.dmi_i[i], i=self.pads.dmi[i], name=f'dmi_i{i}', **ddr) + self.ser(i=self.out.dqs_o[i], o=self.pads.dqs_o[i], name=f'dqs_o{i}', **ddr_90) + self.des(o=self.out.dqs_i[i], i=self.pads.dqs[i], name=f'dqs_i{i}', **ddr_90) + for i in range(self.databits): + self.ser(i=self.out.dq_o[i], o=self.pads.dq_o[i], name=f'dq_o{i}', **ddr) + self.des(o=self.out.dq_i[i], i=self.pads.dq[i], name=f'dq_i{i}', **ddr) + + # Output enable signals + self.comb += [ + self.pads.dmi_oe.eq(delay(self.out.dmi_oe, cycles=Serializer.LATENCY)), + self.pads.dqs_oe.eq(delay(self.out.dqs_oe, cycles=Serializer.LATENCY)), + self.pads.dq_oe.eq(delay(self.out.dq_oe, cycles=Serializer.LATENCY)), + ] diff --git a/litedram/phy/utils.py b/litedram/phy/utils.py index 01eb5e6..c168c0f 100644 --- a/litedram/phy/utils.py +++ b/litedram/phy/utils.py @@ -320,6 +320,21 @@ class Deserializer(Module): sd_clkdiv += self.o.eq(Cat(as_array(o_pre_d)[:-1], as_array(o_pre)[-1])) +class SimSerDesMixin: + """Helper class for easier (de-)serialization to simulation pads.""" + def ser(self, *, i, o, clkdiv, clk, name="", **kwargs): + assert len(o) == 1 + kwargs = dict(i=i, i_dw=len(i), o=o, o_dw=1, clk=clk, clkdiv=clkdiv, + name=f"ser_{name}".strip("_"), **kwargs) + self.submodules += Serializer(**kwargs) + + def des(self, *, i, o, clkdiv, clk, name="", **kwargs): + assert len(i) == 1 + kwargs = dict(i=i, i_dw=1, o=o, o_dw=len(o), clk=clk, clkdiv=clkdiv, + name=f"des_{name}".strip("_"), **kwargs) + self.submodules += Deserializer(**kwargs) + + class SimLogger(Module, AutoCSR): """Logger for use in simulation