Skip to content

Commit ad13d30

Browse files
Merge pull request #110 from litex-hub/feature/spi-nor-flash-model
model: Add pin-level SPI NOR flash emulator
2 parents 91b2d43 + 8c0a248 commit ad13d30

4 files changed

Lines changed: 493 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ PHY:
2727
- Portable/Generic.
2828
- Single/Dual/Quad/Octal SPI Bus support.
2929
- Dynamic Clk frequency configuration and auto-calibration.
30+
- Pin-level SPI NOR flash read model for simulation.
3031

3132
Core:
3233
- Dynamic Crossbar.
@@ -57,4 +58,4 @@ do them if possible:
5758
- cite LiteSPI in publications related to research it has helped
5859
- send us feedback and suggestions for improvements
5960
- send us bug reports when something goes wrong
60-
- send us the modifications and improvements you have done to LiteSPI.
61+
- send us the modifications and improvements you have done to LiteSPI.

litespi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from litespi.crossbar import LiteSPICrossbar
1515
from litespi.core.master import LiteSPIMaster
1616
from litespi.core.mmap import LiteSPIMMAP
17+
from litespi.flash_model import LiteSPINORFlashModel
1718
from litespi.phy.generic import LiteSPIPHY
1819

1920
class LiteSPICore(Module):

litespi/flash_model.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#
2+
# This file is part of LiteSPI.
3+
#
4+
# Copyright (c) 2026 Florent Kermarrec <florent@enjoy-digital.fr>
5+
# SPDX-License-Identifier: BSD-2-Clause
6+
7+
from migen import *
8+
from migen.fhdl.specials import Tristate
9+
10+
from litex.gen import *
11+
12+
13+
# SPI NOR Flash Model ------------------------------------------------------------------------------
14+
15+
class LiteSPINORFlashModel(LiteXModule):
16+
"""Pin-level SPI NOR flash read model.
17+
18+
The model implements the read command selected by ``flash.read_opcode`` and is intended to
19+
exercise the real LiteSPI PHY in Migen simulations. It supports SDR commands with x1/x2/x4/x8
20+
command, address, and data phases, 24-bit or 32-bit addresses, dummy cycles, and continuous
21+
reads while chip select remains active.
22+
23+
``init`` contains byte values starting at flash address zero. Uninitialized locations read as
24+
the erased NOR value, ``0xff``.
25+
26+
The model accepts regular LiteSPI pads (``clk``, ``cs_n`` and either ``mosi``/``miso`` or
27+
``dq``). Simulations can instead provide split ``dq_i``, ``dq_o`` and ``dq_oe`` signals to
28+
avoid resolving bidirectional signals in the simulator.
29+
"""
30+
31+
def __init__(self, pads, flash, init=None):
32+
if flash.ddr:
33+
raise ValueError("LiteSPINORFlashModel only supports SDR read commands")
34+
if len(pads.cs_n) != 1:
35+
raise ValueError("LiteSPINORFlashModel requires exactly one chip-select signal")
36+
37+
init = [] if init is None else list(init)
38+
if len(init) > flash.total_size:
39+
raise ValueError("SPI NOR model initialization exceeds the flash size")
40+
if any(not isinstance(value, int) or value < 0 or value > 0xff for value in init):
41+
raise ValueError("SPI NOR model initialization must contain byte values")
42+
43+
cmd_width = flash.cmd_width
44+
addr_width = flash.addr_width
45+
data_width = flash.bus_width
46+
addr_bits = flash.addr_bits
47+
dummy_cycles = flash.dummy_cycles if flash.fast_mode else 0
48+
49+
if any(width not in [1, 2, 4, 8] for width in [cmd_width, addr_width, data_width]):
50+
raise ValueError("SPI NOR model phase widths must be 1, 2, 4, or 8")
51+
if 8 % cmd_width or addr_bits % addr_width or 8 % data_width:
52+
raise ValueError("SPI NOR model phase lengths must be divisible by their bus widths")
53+
54+
# Normalize regular and split SPI pads to the flash-side DQ signals. DQ0 carries serial
55+
# input and DQ1 carries serial output in x1 mode.
56+
if hasattr(pads, "mosi"):
57+
if any(width != 1 for width in [cmd_width, addr_width, data_width]):
58+
raise ValueError("Separate MOSI/MISO pads only support x1 phases")
59+
dq_i = Signal(2)
60+
dq_o = Signal(2)
61+
dq_oe = Signal(2)
62+
self.comb += [
63+
dq_i[0].eq(pads.mosi),
64+
pads.miso.eq(Mux(dq_oe[1], dq_o[1], 1)),
65+
]
66+
elif all(hasattr(pads, name) for name in ["dq_i", "dq_o", "dq_oe"]):
67+
if len({len(pads.dq_i), len(pads.dq_o), len(pads.dq_oe)}) != 1:
68+
raise ValueError("Split SPI NOR model DQ signals must have matching widths")
69+
dq_i = Signal(len(pads.dq_i))
70+
dq_o = Signal(len(pads.dq_o))
71+
dq_oe = Signal(len(pads.dq_oe))
72+
self.comb += [
73+
dq_i.eq(pads.dq_i),
74+
pads.dq_o.eq(dq_o),
75+
pads.dq_oe.eq(dq_oe),
76+
]
77+
elif hasattr(pads, "dq"):
78+
dq_i = Signal(len(pads.dq))
79+
dq_o = Signal(len(pads.dq))
80+
dq_oe = Signal(len(pads.dq))
81+
for n in range(len(pads.dq)):
82+
self.specials += Tristate(pads.dq[n], dq_o[n], dq_oe[n], dq_i[n])
83+
else:
84+
raise ValueError("SPI NOR model requires MOSI/MISO, DQ, or split DQ pads")
85+
86+
required_width = 2 if data_width == 1 else max(cmd_width, addr_width, data_width)
87+
if len(dq_i) < required_width:
88+
raise ValueError("SPI NOR model pads are too narrow for the configured read command")
89+
90+
self.dq_i = dq_i
91+
self.dq_o = dq_o
92+
self.dq_oe = dq_oe
93+
94+
# Store only initialized bytes; the remainder of the modeled flash reads as erased. This
95+
# keeps simulations of large flash modules lightweight.
96+
mem = Memory(8, max(2, len(init)), init=init or [0xff, 0xff])
97+
read_port = mem.get_port(async_read=True)
98+
read_next_port = mem.get_port(async_read=True)
99+
self.specials += mem, read_port, read_next_port
100+
self.mem = mem
101+
102+
read_addr = Signal(max=flash.total_size)
103+
read_next_addr = Signal.like(read_addr)
104+
read_data = Signal(8)
105+
read_next_data = Signal(8)
106+
self.read_addr = read_addr
107+
self.comb += If(read_addr == (flash.total_size - 1),
108+
read_next_addr.eq(0),
109+
).Else(
110+
read_next_addr.eq(read_addr + 1),
111+
)
112+
if init:
113+
self.comb += [
114+
read_port.adr.eq(Mux(read_addr < len(init), read_addr, 0)),
115+
read_next_port.adr.eq(Mux(read_next_addr < len(init), read_next_addr, 0)),
116+
read_data.eq(Mux(read_addr < len(init), read_port.dat_r, 0xff)),
117+
read_next_data.eq(Mux(read_next_addr < len(init), read_next_port.dat_r, 0xff)),
118+
]
119+
else:
120+
self.comb += [
121+
read_port.adr.eq(0),
122+
read_next_port.adr.eq(0),
123+
read_data.eq(0xff),
124+
read_next_data.eq(0xff),
125+
]
126+
127+
# Detect the externally generated SPI clock edges in the simulation clock domain.
128+
clk_d = Signal()
129+
clk_posedge = Signal()
130+
clk_negedge = Signal()
131+
self.sync += clk_d.eq(pads.clk)
132+
self.comb += [
133+
clk_posedge.eq( pads.clk & ~clk_d),
134+
clk_negedge.eq(~pads.clk & clk_d),
135+
]
136+
137+
command = Signal(8)
138+
command_next = Signal(8)
139+
address = Signal(addr_bits)
140+
address_next = Signal(addr_bits)
141+
phase_count = Signal(max=max(addr_bits, 8) + 1)
142+
dummy_count = Signal(max=max(dummy_cycles, 1) + 1)
143+
data_count = Signal(max=8)
144+
data_shift = Signal(8)
145+
data_sampled = Signal()
146+
self.command = command
147+
148+
self.sync += If(pads.cs_n,
149+
command.eq(0),
150+
address.eq(0),
151+
read_addr.eq(0),
152+
phase_count.eq(0),
153+
dummy_count.eq(0),
154+
data_count.eq(0),
155+
data_shift.eq(0),
156+
data_sampled.eq(0),
157+
)
158+
159+
self.comb += [
160+
command_next.eq(Cat(dq_i[:cmd_width], command)[:8]),
161+
address_next.eq(Cat(dq_i[:addr_width], address)[:addr_bits]),
162+
dq_o.eq(0),
163+
dq_oe.eq(0),
164+
]
165+
166+
self.submodules.fsm = fsm = ResetInserter()(FSM(reset_state="COMMAND"))
167+
self.comb += fsm.reset.eq(pads.cs_n)
168+
169+
if dummy_cycles:
170+
after_address = [
171+
NextValue(dummy_count, 0),
172+
NextState("DUMMY"),
173+
]
174+
else:
175+
after_address = [NextState("DATA-LOAD")]
176+
177+
fsm.act("COMMAND",
178+
If(clk_posedge,
179+
NextValue(command, command_next),
180+
If(phase_count == (8 - cmd_width),
181+
NextValue(phase_count, 0),
182+
If(command_next == flash.read_opcode.code,
183+
NextState("ADDRESS"),
184+
).Else(
185+
NextState("IGNORE"),
186+
),
187+
).Else(
188+
NextValue(phase_count, phase_count + cmd_width),
189+
),
190+
),
191+
)
192+
193+
fsm.act("ADDRESS",
194+
If(clk_posedge,
195+
NextValue(address, address_next),
196+
If(phase_count == (addr_bits - addr_width),
197+
NextValue(phase_count, 0),
198+
NextValue(read_addr, address_next),
199+
*after_address,
200+
).Else(
201+
NextValue(phase_count, phase_count + addr_width),
202+
),
203+
),
204+
)
205+
206+
if dummy_cycles:
207+
fsm.act("DUMMY",
208+
If(clk_posedge,
209+
If(dummy_count == (dummy_cycles - 1),
210+
NextValue(dummy_count, 0),
211+
NextState("DATA-LOAD"),
212+
).Else(
213+
NextValue(dummy_count, dummy_count + 1),
214+
),
215+
),
216+
)
217+
218+
fsm.act("DATA-LOAD",
219+
NextValue(data_shift, read_data),
220+
NextValue(data_count, 0),
221+
NextValue(data_sampled, 0),
222+
NextState("DATA"),
223+
)
224+
225+
output_mask = 0b10 if data_width == 1 else 2**data_width - 1
226+
output_start = 7 if data_width == 1 else 8 - data_width
227+
fsm.act("DATA",
228+
dq_oe.eq(output_mask),
229+
dq_o.eq(data_shift[output_start:8] << (1 if data_width == 1 else 0)),
230+
If(clk_posedge,
231+
NextValue(data_sampled, 1),
232+
),
233+
If(clk_negedge & data_sampled,
234+
NextValue(data_sampled, 0),
235+
If(data_count == (8 - data_width),
236+
NextValue(read_addr, read_next_addr),
237+
NextValue(data_shift, read_next_data),
238+
NextValue(data_count, 0),
239+
).Else(
240+
NextValue(data_count, data_count + data_width),
241+
NextValue(data_shift, data_shift << data_width),
242+
),
243+
),
244+
)
245+
246+
# Unknown commands are ignored until the controller releases chip select.
247+
fsm.act("IGNORE")

0 commit comments

Comments
 (0)