Skip to content

Commit 715e287

Browse files
Merge pull request #106 from litex-hub/feature/ddr-phy-clock-divider
phy/generic_ddr: Add configurable SCK divider
2 parents 4b671db + 31088b3 commit 715e287

4 files changed

Lines changed: 481 additions & 110 deletions

File tree

litespi/clkgen.py

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,110 @@ class DDRLiteSPIClkGen(LiteXModule):
2222
pads : Object
2323
SPI pads description.
2424
25+
div_width : int
26+
Width of the ``div`` input.
27+
28+
extra_latency : int or float
29+
Additional input latency in half-system-clock steps.
30+
2531
Attributes
2632
----------
33+
div : Signal(), in
34+
Clock divisor. SCK frequency is ``sys_clk_freq/div``.
35+
2736
en : Signal(), in
28-
Clock enable input, output clock will be generated if set to 1, 0 resets the core.
37+
Keep the clock generator active.
38+
39+
start : Signal(), in
40+
Start a transfer and latch ``div``.
41+
42+
posedge : Signal(), out
43+
Indicates an SCK rising edge at the current system rising edge.
44+
45+
negedge : Signal(2), out
46+
Indicates an SCK falling edge before lane 0 or between lanes 0 and 1.
47+
48+
sample : Signal(), out
49+
Delayed ``posedge`` used to capture the registered DQ inputs.
2950
"""
30-
def __init__(self, pads):
31-
self.en = en = Signal()
51+
def __init__(self, pads, div_width=8, extra_latency=0):
52+
self.div = div = Signal(div_width)
53+
self.en = en = Signal()
54+
self.start = start = Signal()
55+
self.posedge = posedge = Signal()
56+
self.negedge = negedge = Signal(2)
57+
self.sample = sample = Signal()
58+
self.clk = clk = Signal(2)
59+
60+
extra_latency_cycles = int(2*extra_latency)
61+
if (extra_latency < 0) or (extra_latency_cycles != 2*extra_latency):
62+
raise ValueError("DDR PHY extra_latency must be a non-negative multiple of 0.5")
63+
64+
active = Signal()
65+
level = Signal(reset=1)
66+
remaining = Signal(div_width, reset=1)
67+
div_latched = Signal(div_width, reset=1)
68+
div_start = Signal(div_width)
69+
slot1_level = Signal()
70+
slot1_remaining = Signal(div_width)
71+
next_level = Signal()
72+
next_remaining = Signal(div_width)
73+
74+
self.comb += div_start.eq(Mux(div == 0, 1, div))
75+
76+
# Consume two SCK half-cycles per system cycle.
77+
self.comb += [
78+
slot1_level.eq(level),
79+
slot1_remaining.eq(remaining - 1),
80+
If(remaining == 1,
81+
slot1_level.eq(~level),
82+
slot1_remaining.eq(div_latched),
83+
),
84+
next_level.eq(slot1_level),
85+
next_remaining.eq(slot1_remaining - 1),
86+
If(slot1_remaining == 1,
87+
next_level.eq(~slot1_level),
88+
next_remaining.eq(div_latched),
89+
),
90+
clk[0].eq(active & level),
91+
clk[1].eq(active & slot1_level),
92+
]
3293

33-
# Keep SCK rising/falling edges aligned with sys rising/falling edges respectively.
34-
self.specials += DDROutput(i1=en, i2=0, o=pads.clk)
94+
# Rising edges stay aligned to system rising edges. Falling edges occur either at the
95+
# beginning of lane 0 for even divisors or between lanes 0/1 for odd divisors.
96+
self.comb += [
97+
posedge.eq(active & level & (remaining == div_latched)),
98+
negedge[0].eq(active & ~level & (remaining == div_latched)),
99+
negedge[1].eq(active & level & (remaining == 1)),
100+
]
101+
102+
self.sync += If(start,
103+
active.eq(1),
104+
level.eq(1),
105+
remaining.eq(div_start),
106+
div_latched.eq(div_start),
107+
).Elif(active,
108+
If(en,
109+
level.eq(next_level),
110+
remaining.eq(next_remaining),
111+
).Else(
112+
active.eq(0),
113+
level.eq(1),
114+
remaining.eq(div_latched),
115+
)
116+
)
117+
118+
# Delay input captures through the IDDR/fabric pipeline. Half-step latency values map to
119+
# complete system cycles since the DDR path handles two half-cycles per system cycle.
120+
sample_pipeline = Signal(2 + extra_latency_cycles)
121+
self.sync += If(start,
122+
sample_pipeline.eq(0),
123+
).Else(
124+
sample_pipeline.eq(Cat(sample_pipeline[1:], posedge)),
125+
)
126+
self.comb += sample.eq(sample_pipeline[0])
127+
128+
self.specials += DDROutput(i1=clk[0], i2=clk[1], o=pads.clk)
35129

36130

37131
class LiteSPIClkGen(LiteXModule):

litespi/phy/generic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(self, pads, flash, device="xc7", clock_domain="sys", rate="1:1", **
6161
if rate == "1:1":
6262
phy = LiteSPISDRPHYCore(pads, flash, device, clock_domain, **kwargs)
6363
if rate == "1:2":
64-
phy = LiteSPIDDRPHYCore(pads, flash, **kwargs)
64+
phy = LiteSPIDDRPHYCore(pads, flash, clock_domain=clock_domain, **kwargs)
6565

6666
self.flash = flash
6767

litespi/phy/generic_ddr.py

Lines changed: 118 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
# This file is part of LiteSPI
33
#
44
# Copyright (c) 2020 Antmicro <www.antmicro.com>
5+
# Copyright (c) 2025-2026 Fin Maaß <f.maass@vogl-electronic.com>
56
# SPDX-License-Identifier: BSD-2-Clause
67

78
from migen import *
89

910
from litex.gen import *
1011

11-
from litex.gen.genlib.misc import WaitTimer
12-
1312
from litespi.common import *
1413
from litespi.clkgen import DDRLiteSPIClkGen
1514
from litespi.cscontrol import LiteSPICSControl
1615

1716
from litex.soc.interconnect import stream
17+
from litex.soc.interconnect.csr import *
1818

1919
from litex.build.io import DDRTristate
2020

@@ -41,8 +41,14 @@ class LiteSPIDDRPHYCore(LiteXModule):
4141
flash : SpiNorFlashModule
4242
SpiNorFlashModule configuration object.
4343
44-
extra_latency : int
45-
Additional input pipeline latency. Each unit extends the pipeline drain by two sys cycles.
44+
extra_latency : int or float
45+
Additional input pipeline latency. Each 0.5 adds one system-clock cycle.
46+
47+
clock_domain : str
48+
Name of the LiteSPI clock domain.
49+
50+
default_divisor : int
51+
Default SCK divisor.
4652
4753
Attributes
4854
----------
@@ -54,11 +60,27 @@ class LiteSPIDDRPHYCore(LiteXModule):
5460
5561
cs : Signal(), in
5662
Flash CS signal.
63+
64+
clk_divisor : CSRStorage
65+
Register which holds the default SCK divisor.
5766
"""
58-
def __init__(self, pads, flash, extra_latency=0, **kwargs):
59-
self.source = source = stream.Endpoint(spi_phy2core_layout)
60-
self.sink = sink = stream.Endpoint(spi_core2phy_layout)
61-
self.cs = Signal().like(pads.cs_n)
67+
def __init__(self, pads, flash, extra_latency=0, clock_domain="sys", default_divisor=1, **kwargs):
68+
self.source = source = stream.Endpoint(spi_phy2core_layout)
69+
self.sink = sink = stream.Endpoint(spi_core2phy_layout)
70+
self.cs = Signal().like(pads.cs_n)
71+
self._spi_clk_divisor = spi_clk_divisor = Signal(len(sink.clk_div))
72+
73+
self._default_divisor = default_divisor
74+
self.clk_divisor = CSRStorage(
75+
len(sink.clk_div),
76+
reset=self._default_divisor,
77+
description="SPI clock divisor.",
78+
)
79+
80+
# # #
81+
82+
# Resynchronize CSR Clock Divisor to LiteSPI Clock Domain.
83+
self.submodules += ResyncReg(self.clk_divisor.storage, spi_clk_divisor, clock_domain)
6284

6385
if hasattr(pads, "miso"):
6486
bus_width = 1
@@ -75,12 +97,17 @@ def __init__(self, pads, flash, extra_latency=0, **kwargs):
7597
assert not flash.ddr
7698

7799
# Clock Generator.
78-
self.clkgen = clkgen = DDRLiteSPIClkGen(pads)
100+
self.clkgen = clkgen = DDRLiteSPIClkGen(
101+
pads = pads,
102+
div_width = len(sink.clk_div),
103+
extra_latency = extra_latency,
104+
)
105+
self.comb += clkgen.div.eq(spi_clk_divisor)
79106

80107
# CS control.
81108
self.cs_control = cs_control = LiteSPICSControl(pads, self.cs, **kwargs)
82109

83-
# Lane 0 is aligned with SCK high/rising and lane 1 with SCK low/falling.
110+
# Lane 0 is output while sys is high and lane 1 while sys is low.
84111
dq_o = Array([Signal(len(dq)) for _ in range(2)])
85112
dq_i = Array([Signal(len(dq)) for _ in range(2)])
86113
dq_oe = Signal(len(dq))
@@ -93,98 +120,114 @@ def __init__(self, pads, flash, extra_latency=0, **kwargs):
93120
)
94121

95122
# Data Shift Registers.
96-
sr_cnt = Signal(8, reset_less=True)
97-
sr_out_load = Signal()
98-
sr_out_shift = Signal()
99-
sr_out = Signal(len(sink.data), reset_less=True)
100-
sr_in_shift = Signal()
101-
sr_in = Signal(len(sink.data), reset_less=True)
102-
input_pipeline = 2 + 2*extra_latency
103-
104-
# Data Out Shift.
105-
self.comb += [
106-
dq_oe.eq(sink.mask),
123+
sr_out_cnt = Signal(len(sink.len), reset_less=True)
124+
sr_in_cnt = Signal(len(sink.len), reset_less=True)
125+
sr_out_load = Signal()
126+
sr_out_shift = Signal()
127+
sr_out = Signal(len(sink.data), reset_less=True)
128+
sr_out_shifted = Signal(len(sink.data), reset_less=True)
129+
sr_in_shift = Signal()
130+
sr_in = Signal(len(sink.data), reset_less=True)
131+
current_data = Signal(len(dq))
132+
next_data = Signal(len(dq))
133+
last_width = Signal.like(sink.width)
134+
no_read = Signal()
135+
136+
# Determine if the transfer only drives DQ and can skip the input-pipeline drain.
137+
self.sync += If(sr_out_load,
107138
Case(sink.width, {
108-
1: dq_o[1].eq(sr_out[-1:]),
109-
2: dq_o[1].eq(sr_out[-2:]),
110-
4: dq_o[1].eq(sr_out[-4:]),
111-
8: dq_o[1].eq(sr_out[-8:]),
139+
"default" : no_read.eq(0),
140+
1 : no_read.eq(sink.mask[0]),
141+
2 : no_read.eq(sink.mask[:2] == 0b11),
142+
4 : no_read.eq(sink.mask[:4] == 0b1111),
143+
8 : no_read.eq(sink.mask == 0xff),
112144
})
145+
)
146+
147+
# Keep DQ stable until SCK falls. For odd divisors SCK falls between the two DDR lanes;
148+
# for even divisors it falls at the beginning of lane 0.
149+
self.comb += [
150+
sr_out_shifted.eq(sr_out << last_width),
151+
Case(last_width, {
152+
1 : [current_data.eq(sr_out[-1:]), next_data.eq(sr_out_shifted[-1:])],
153+
2 : [current_data.eq(sr_out[-2:]), next_data.eq(sr_out_shifted[-2:])],
154+
4 : [current_data.eq(sr_out[-4:]), next_data.eq(sr_out_shifted[-4:])],
155+
8 : [current_data.eq(sr_out[-8:]), next_data.eq(sr_out_shifted[-8:])],
156+
}),
157+
dq_o[0].eq(Mux(clkgen.negedge[0], next_data, current_data)),
158+
dq_o[1].eq(Mux(clkgen.negedge != 0, next_data, current_data)),
113159
]
160+
114161
self.sync += If(sr_out_load,
115-
sr_out.eq(sink.data << (len(sink.data) - sink.len))
116-
)
117-
self.sync += If(sr_out_shift,
118-
dq_o[0].eq(dq_o[1]),
119-
Case(sink.width, {
120-
1 : sr_out.eq(Cat(Signal(1), sr_out)),
121-
2 : sr_out.eq(Cat(Signal(2), sr_out)),
122-
4 : sr_out.eq(Cat(Signal(4), sr_out)),
123-
8 : sr_out.eq(Cat(Signal(8), sr_out)),
124-
})
162+
sr_out.eq(sink.data << (len(sink.data) - sink.len)),
163+
sr_in.eq(0),
164+
sr_out_cnt.eq(sink.len),
165+
sr_in_cnt.eq(sink.len),
166+
).Elif(sr_out_shift,
167+
sr_out.eq(sr_out_shifted),
168+
sr_out_cnt.eq(sr_out_cnt - last_width),
125169
)
126170

127-
# Data In Shift.
171+
# SCK rising edges are always aligned to lane 0.
128172
self.sync += If(sr_in_shift,
129-
Case(sink.width, {
173+
Case(last_width, {
130174
1 : sr_in.eq(Cat(dq_i[0][1], sr_in)), # 1: pads.miso
131175
2 : sr_in.eq(Cat(dq_i[0][:2], sr_in)),
132176
4 : sr_in.eq(Cat(dq_i[0][:4], sr_in)),
133177
8 : sr_in.eq(Cat(dq_i[0][:8], sr_in)),
134-
})
178+
}),
179+
sr_in_cnt.eq(sr_in_cnt - last_width),
135180
)
136181

137182
# FSM.
138183
self.fsm = fsm = FSM(reset_state="WAIT-CMD-DATA")
139184
fsm.act("WAIT-CMD-DATA",
140-
# Stop Clk.
141-
NextValue(clkgen.en, 0),
142-
# Wait for CS and a CMD from the Core.
185+
# Wait for CS and a command from the Core.
143186
If(cs_control.enable & sink.valid,
144-
# Load Shift Register Count/Data Out.
145-
NextValue(sr_cnt, sink.len - sink.width),
187+
clkgen.start.eq(1),
188+
NextValue(last_width, sink.width),
189+
NextValue(dq_oe, sink.mask),
146190
sr_out_load.eq(1),
147-
# Start XFER.
148-
NextState("XFER")
191+
sink.ready.eq(1),
192+
If(sink.clk_div > 0,
193+
clkgen.div.eq(sink.clk_div),
194+
),
195+
NextState("XFER"),
149196
)
150197
)
151-
152198
fsm.act("XFER",
153-
# Generate Clk.
154-
NextValue(clkgen.en, 1),
155-
156-
# Data In Shift.
157-
sr_in_shift.eq(1),
158-
159-
# Data Out Shift.
160-
sr_out_shift.eq(1),
161-
162-
# Shift Register Count Update/Check.
163-
NextValue(sr_cnt, sr_cnt - sink.width),
164-
# End XFer.
165-
If(sr_cnt == 0,
166-
# Drain the IDDR and fabric input pipeline after the final SCK edge.
167-
NextValue(sr_cnt, input_pipeline*sink.width),
168-
NextState("XFER-END"),
199+
clkgen.en.eq(1),
200+
201+
# Capture registered input data after each delayed SCK rising edge.
202+
If(clkgen.sample,
203+
sr_in_shift.eq(1),
169204
),
205+
206+
# Advance output data after each SCK falling edge.
207+
If(clkgen.negedge != 0,
208+
sr_out_shift.eq(1),
209+
If(sr_out_cnt == last_width,
210+
clkgen.en.eq(0),
211+
NextValue(dq_oe, 0),
212+
If(no_read | (sr_in_cnt == 0) | (clkgen.sample & (sr_in_cnt == last_width)),
213+
NextState("SEND-STATUS-DATA"),
214+
).Else(
215+
NextState("XFER-END"),
216+
)
217+
)
218+
)
170219
)
171220
fsm.act("XFER-END",
172-
# Stop Clk.
173-
NextValue(clkgen.en, 0),
174-
175-
# Data In Shift.
176-
sr_in_shift.eq(1),
177-
178-
# Shift Register Count Update/Check.
179-
NextValue(sr_cnt, sr_cnt - sink.width),
180-
If(sr_cnt == 0,
181-
sink.ready.eq(1),
182-
NextState("SEND-STATUS-DATA"),
183-
),
221+
# Drain input events that are still crossing the IDDR/fabric pipeline.
222+
If(clkgen.sample,
223+
sr_in_shift.eq(1),
224+
If(sr_in_cnt == last_width,
225+
NextState("SEND-STATUS-DATA"),
226+
)
227+
)
184228
)
185229
self.comb += source.data.eq(sr_in)
186230
fsm.act("SEND-STATUS-DATA",
187-
# Send Data In to Core and return to WAIT when accepted.
188231
source.valid.eq(1),
189232
If(source.ready,
190233
NextState("WAIT-CMD-DATA"),

0 commit comments

Comments
 (0)