Skip to content

Commit 9e83eea

Browse files
committed
phy: Align Nexus DDR writes without altering CWL
The Nexus write path is one memory clock later than the command path. Compensating for this by incrementing CWL programs a different DDR3 mode and can produce an invalid CWL for the selected memory clock. Schedule DFI write data one sys_clk cycle earlier, then apply a fixed half-word slip to DQ, DM, DQS and their tristate controls. Since a sys_clk cycle spans two memory clocks, this advances the complete write stream by the required single memory clock while preserving the JEDEC CWL. Add simulations for data and tristate bit ordering, burst preamble/postamble boundaries, and the default Nexus PHY timing settings. Suggested-by: Myrtle Shah <gatecat@ds0.me>
1 parent c9d753e commit 9e83eea

2 files changed

Lines changed: 142 additions & 19 deletions

File tree

litedram/phy/nxddrphy.py

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from litedram.common import *
2626
from litedram.phy.dfi import *
27+
from litedram.phy.utils import ConstBitSlip
2728

2829
# BitSlip ------------------------------------------------------------------------------------------
2930

@@ -49,6 +50,18 @@ def __init__(self, dw, rst=None, slp=None, cycles=1):
4950
cases[i] = self.o.eq(r[i:dw+i])
5051
self.comb += Case(value, cases)
5152

53+
# Nexus DDR PHY Write BitSlip ----------------------------------------------------------------------
54+
55+
class _NexusDDRPHYWriteBitSlip(ConstBitSlip):
56+
def __init__(self, dw):
57+
assert (dw % 2) == 0
58+
ConstBitSlip.__init__(self,
59+
dw = dw,
60+
slp = dw//2,
61+
cycles = 1,
62+
register = False,
63+
)
64+
5265
# Lattice Nexus DDR PHY Initialization --------------------------------------------------------------
5366

5467
class NexusDDRPHYInit(Module):
@@ -170,10 +183,6 @@ def __init__(self, pads,
170183
rdphase = get_sys_phase(nphases, cl_sys_latency, cl)
171184
wrphase = get_sys_phase(nphases, cwl_sys_latency, cwl)
172185

173-
# Otherwise writes are off by half a SCLK cycle
174-
# TODO: should we implement write DQ/DQS bitslip like s7ddrphy?
175-
cwl += 1
176-
177186
self.settings = PhySettings(
178187
phytype = "NexusDDRPHY",
179188
memtype = memtype,
@@ -186,7 +195,7 @@ def __init__(self, pads,
186195
cl = cl,
187196
cwl = cwl,
188197
read_latency = cl_sys_latency + 9,
189-
write_latency = cwl_sys_latency,
198+
write_latency = cwl_sys_latency - 1,
190199
read_leveling = True,
191200
bitslips = 4,
192201
delays = 16,
@@ -329,28 +338,37 @@ def __init__(self, pads,
329338
]
330339

331340
# DQS ----------------------------------------------------------------------------------
332-
dqs = Signal()
333-
dqs_oe_n = Signal()
341+
dqs = Signal()
342+
dqs_oe_n = Signal()
343+
dqs_o_data = Signal(4)
344+
dqs_oe_n_data = Signal(2)
345+
dqs_o_bitslip = _NexusDDRPHYWriteBitSlip(4)
346+
dqs_oe_n_bitslip = _NexusDDRPHYWriteBitSlip(2)
347+
self.submodules += dqs_o_bitslip, dqs_oe_n_bitslip
348+
self.comb += [
349+
dqs_o_data.eq(Cat(0, dqs_oe, 0, dqs_oe | dqs_preamble)),
350+
dqs_oe_n_data.eq(Cat(
351+
~(dqs_oe | dqs_postamble),
352+
~(dqs_oe | dqs_preamble),
353+
)),
354+
dqs_o_bitslip.i.eq(dqs_o_data),
355+
dqs_oe_n_bitslip.i.eq(dqs_oe_n_data),
356+
]
334357
self.specials += [
335358
Instance("ODDRX2DQS",
336359
i_RST = ResetSignal("sys"),
337360
i_SCLK = ClockSignal("sys"),
338361
i_ECLK = ClockSignal("sys2x"),
339362
i_DQSW = dqsw,
340-
i_D0 = 0,
341-
i_D1 = dqs_oe,
342-
i_D2 = 0,
343-
i_D3 = dqs_oe | dqs_preamble,
344-
# **{f"i_D{n}": (0b1010 >> n) & 0b1 for n in range(4)},
363+
**{f"i_D{n}": dqs_o_bitslip.o[n] for n in range(4)},
345364
o_Q = dqs
346365
),
347366
Instance("TSHX2DQS",
348367
i_RST = ResetSignal("sys"),
349368
i_SCLK = ClockSignal("sys"),
350369
i_ECLK = ClockSignal("sys2x"),
351370
i_DQSW = dqsw,
352-
i_T0 = ~(dqs_oe | dqs_postamble),
353-
i_T1 = ~(dqs_oe | dqs_preamble),
371+
**{f"i_T{n}": dqs_oe_n_bitslip.o[n] for n in range(2)},
354372
o_Q = dqs_oe_n
355373
),
356374
Tristate(pads.dqs_p[i], dqs, ~dqs_oe_n, dqs_i)
@@ -360,8 +378,11 @@ def __init__(self, pads,
360378
dm_o_data = Signal(8)
361379
dm_o_data_d = Signal(8)
362380
dm_o_data_muxed = Signal(4)
381+
dm_o_bitslip = _NexusDDRPHYWriteBitSlip(4)
382+
self.submodules += dm_o_bitslip
363383
for n in range(8):
364384
self.comb += dm_o_data[n].eq(dfi.phases[n//4].wrdata_mask[n%4*databits//8+dm_remapping.get(i, i)])
385+
self.comb += dm_o_bitslip.i.eq(dm_o_data_muxed)
365386
self.sync += dm_o_data_d.eq(dm_o_data)
366387
dm_bl8_cases = {}
367388
dm_bl8_cases[0] = dm_o_data_muxed.eq(dm_o_data[:4])
@@ -372,7 +393,7 @@ def __init__(self, pads,
372393
i_SCLK = ClockSignal("sys"),
373394
i_ECLK = ClockSignal("sys2x"),
374395
i_DQSW270 = dqsw270,
375-
**{f"i_D{n}": dm_o_data_muxed[n] for n in range(4)},
396+
**{f"i_D{n}": dm_o_bitslip.o[n] for n in range(4)},
376397
o_Q = pads.dm[i]
377398
)
378399

@@ -386,8 +407,20 @@ def __init__(self, pads,
386407
dq_o_data = Signal(8)
387408
dq_o_data_d = Signal(8)
388409
dq_o_data_muxed = Signal(4)
410+
dq_oe_n_data = Signal(2)
411+
dq_o_bitslip = _NexusDDRPHYWriteBitSlip(4)
412+
dq_oe_n_bitslip = _NexusDDRPHYWriteBitSlip(2)
413+
self.submodules += dq_o_bitslip, dq_oe_n_bitslip
389414
for n in range(8):
390415
self.comb += dq_o_data[n].eq(dfi.phases[n//4].wrdata[n%4*databits+j])
416+
self.comb += [
417+
dq_oe_n_data.eq(Cat(
418+
~(dq_oe | dqs_postamble),
419+
~(dq_oe | dqs_preamble),
420+
)),
421+
dq_o_bitslip.i.eq(dq_o_data_muxed),
422+
dq_oe_n_bitslip.i.eq(dq_oe_n_data),
423+
]
391424
self.sync += dq_o_data_d.eq(dq_o_data)
392425
dq_bl8_cases = {}
393426
dq_bl8_cases[0] = dq_o_data_muxed.eq(dq_o_data[:4])
@@ -399,7 +432,7 @@ def __init__(self, pads,
399432
i_SCLK = ClockSignal("sys"),
400433
i_ECLK = ClockSignal("sys2x"),
401434
i_DQSW270 = dqsw270,
402-
**{f"i_D{n}": dq_o_data_muxed[n] for n in range(4)},
435+
**{f"i_D{n}": dq_o_bitslip.o[n] for n in range(4)},
403436
o_Q = dq_o
404437
)
405438
]
@@ -437,8 +470,7 @@ def __init__(self, pads,
437470
i_SCLK = ClockSignal("sys"),
438471
i_ECLK = ClockSignal("sys2x"),
439472
i_DQSW270 = dqsw270,
440-
i_T0 = ~(dq_oe | dqs_postamble),
441-
i_T1 = ~(dq_oe | dqs_preamble),
473+
**{f"i_T{n}": dq_oe_n_bitslip.o[n] for n in range(2)},
442474
o_Q = dq_oe_n,
443475
),
444476
Tristate(pads.dq[j], dq_o, ~dq_oe_n, dq_i)
@@ -466,7 +498,9 @@ def __init__(self, pads,
466498
self.comb += dqs_re.eq(rddata_en.taps[rdtap] | rddata_en.taps[rdtap + 1])
467499

468500
# Write Control Path -----------------------------------------------------------------------
469-
wrtap = cwl_sys_latency
501+
# The Nexus write path is one memory-clock late. Start one sys_clk cycle early and use the
502+
# fixed half-word bitslips above to place DQ, DM, DQS and their tristates at the JEDEC CWL.
503+
wrtap = cwl_sys_latency - 1
470504

471505
# Create a delay line of write commands coming from the DFI interface. This taps are used to
472506
# control DQ/DQS tristates and to select write data of the DRAM burst from the DFI interface.

test/test_nxddrphy.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#
2+
# This file is part of LiteDRAM.
3+
#
4+
# Copyright (c) 2026 Florent Kermarrec <florent@enjoy-digital.fr>
5+
# SPDX-License-Identifier: BSD-2-Clause
6+
7+
import unittest
8+
9+
from migen import *
10+
from migen.sim import run_simulation
11+
12+
from litedram.phy.nxddrphy import NexusDDRPHY, _NexusDDRPHYWriteBitSlip
13+
14+
15+
class TestNexusDDRPHYWriteBitSlip(unittest.TestCase):
16+
@staticmethod
17+
def run_bitslip(dw, inputs, idle=0):
18+
dut = _NexusDDRPHYWriteBitSlip(dw)
19+
outputs = []
20+
21+
def generator():
22+
# Fill the history with the inactive value used by the stream.
23+
yield dut.i.eq(idle)
24+
yield
25+
for current in inputs:
26+
yield dut.i.eq(current)
27+
yield
28+
outputs.append((yield dut.o))
29+
30+
run_simulation(dut, generator())
31+
return outputs
32+
33+
def test_half_word_shift(self):
34+
for dw, inputs in [
35+
(4, [0b1101, 0b0011, 0b1010, 0b0110]),
36+
(2, [0b11, 0b00, 0b10, 0b01]),
37+
]:
38+
with self.subTest(dw=dw):
39+
outputs = self.run_bitslip(dw, inputs)
40+
41+
half = dw//2
42+
mask = (1 << half) - 1
43+
expected = []
44+
previous = 0
45+
for current in inputs:
46+
expected.append((previous >> half) | ((current & mask) << half))
47+
previous = current
48+
self.assertEqual(outputs, expected)
49+
50+
def test_one_memory_clock_advance(self):
51+
streams = [
52+
# DQS data: idle, preamble, data, data, postamble, idle.
53+
(4, 0b0000, [0b0000, 0b1000, 0b1010, 0b1010, 0b0000, 0b0000]),
54+
# DQS tristate: T0 is serialized before T1.
55+
(2, 0b0011, [0b0011, 0b0001, 0b0000, 0b0000, 0b0010, 0b0011]),
56+
]
57+
for dw, idle, original in streams:
58+
with self.subTest(dw=dw):
59+
early = original[1:] + [idle]
60+
shifted = self.run_bitslip(dw, early, idle=idle)
61+
62+
original_bits = [((word >> n) & 1) for word in original for n in range(dw)]
63+
shifted_bits = [((word >> n) & 1) for word in shifted for n in range(dw)]
64+
half = dw//2
65+
idle_bits = [((idle >> n) & 1) for n in range(half)]
66+
self.assertEqual(shifted_bits, original_bits[half:] + idle_bits)
67+
68+
69+
class TestNexusDDRPHYSettings(unittest.TestCase):
70+
@staticmethod
71+
def get_pads():
72+
return Record([
73+
("a", 14),
74+
("ba", 3),
75+
("ras_n", 1),
76+
("cas_n", 1),
77+
("we_n", 1),
78+
("clk_p", 1),
79+
("dq", 8),
80+
("dm", 1),
81+
("dqs_p", 1),
82+
])
83+
84+
def test_standard_cwl(self):
85+
dut = NexusDDRPHY(self.get_pads(), sys_clk_freq=75e6)
86+
87+
self.assertEqual(dut.settings.cwl, 5)
88+
self.assertEqual(dut.settings.wrphase, 1)
89+
self.assertEqual(dut.settings.write_latency, 2)

0 commit comments

Comments
 (0)