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
78from migen import *
89
910from litex .gen import *
1011
11- from litex .gen .genlib .misc import WaitTimer
12-
1312from litespi .common import *
1413from litespi .clkgen import DDRLiteSPIClkGen
1514from litespi .cscontrol import LiteSPICSControl
1615
1716from litex .soc .interconnect import stream
17+ from litex .soc .interconnect .csr import *
1818
1919from 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