-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathspi_rom.py
303 lines (286 loc) · 11 KB
/
spi_rom.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
from nmigen import *
from math import ceil, log2
from nmigen.back.pysim import *
from nmigen_soc.memory import *
from nmigen_soc.wishbone import *
from nmigen_boards.resources import *
from isa import *
###########################
# SPI Flash "ROM" module: #
###########################
# (Dummy SPI resources for simulated tests)
class DummyPin():
def __init__( self, name ):
self.o = Signal( name = '%s_o'%name )
self.i = Signal( name = '%s_i'%name )
class DummySPI():
def __init__( self ):
self.cs = DummyPin( 'cs' )
self.clk = DummyPin( 'clk' )
self.mosi = DummyPin( 'mosi' )
self.miso = DummyPin( 'miso' )
# Core SPI Flash "ROM" module.
class SPI_ROM( Elaboratable ):
def __init__( self, dat_start, dat_end, data ):
# Starting address in the Flash chip. This probably won't
# be zero, because many FPGA boards use their external SPI
# Flash to store the bitstream which configures the chip.
self.dstart = dat_start
# Last accessible address in the flash chip.
self.dend = dat_end
# Length of accessible data.
self.dlen = ( dat_end - dat_start ) + 1
# SPI Flash address command.
self.spio = Signal( 32, reset = 0x03000000 )
# Data counter.
self.dc = Signal( 6, reset = 0b000000 )
# Backing data store for a test ROM image. Not used when
# the module is built for real hardware.
if data is not None:
self.data = Memory( width = 32, depth = len( data ), init = data )
else:
self.data = None
# Initialize Wishbone bus arbiter.
self.arb = Arbiter( addr_width = ceil( log2( self.dlen + 1 ) ),
data_width = 32 )
self.arb.bus.memory_map = MemoryMap(
addr_width = self.arb.bus.addr_width,
data_width = self.arb.bus.data_width,
alignment = 0 )
def new_bus( self ):
# Initialize a new Wishbone bus interface.
bus = Interface( addr_width = self.arb.bus.addr_width,
data_width = self.arb.bus.data_width )
bus.memory_map = MemoryMap( addr_width = bus.addr_width,
data_width = bus.data_width,
alignment = 0 )
self.arb.add( bus )
return bus
def elaborate( self, platform ):
m = Module()
m.submodules.arb = self.arb
if platform is None:
self.spi = DummySPI()
else:
self.spi = platform.request( 'spi_flash_1x' )
# Clock rests at 0.
m.d.comb += self.spi.clk.o.eq( 0 )
# Use a state machine for Flash access.
# "Mode 0" SPI is very simple:
# - Device is active when CS is low, inactive otherwise.
# - Clock goes low, both sides write their bit if necessary.
# - Clock goes high, both sides read their bit if necessary.
# - Repeat ad nauseum.
with m.FSM() as fsm:
# 'Reset' and 'power-up' states:
# pull CS low, then release power-down mode by sending 0xAB.
# Normally this is not necessary, but iCE40 chips shut down
# their connected SPI Flash after configuring themselves
# in order to save power and prevent unintended writes.
with m.State( "SPI_RESET" ):
m.d.sync += [
self.spi.cs.o.eq( 1 ),
self.spio.eq( 0xAB000000 )
]
m.next = "SPI_POWERUP"
with m.State( "SPI_POWERUP" ):
m.d.comb += [
self.spi.clk.o.eq( ~ClockSignal( "sync" ) ),
self.spi.mosi.o.eq( self.spio[ 31 ] )
]
m.d.sync += [
self.spio.eq( self.spio << 1 ),
self.dc.eq( self.dc + 1 )
]
m.next = "SPI_POWERUP"
# Wait a few extra cycles after ending the transaction to
# allow the chip to wake up from sleep mode.
# TODO: Time this based on clock frequency?
with m.If( self.dc == 30 ):
m.next = "SPI_WAITING"
# De-assert CS after sending 8 bits of data = 16 clock edges.
with m.Elif( self.dc >= 8 ):
m.d.sync += self.spi.cs.o.eq( 0 )
# 'Waiting' state: Keep the 'cs' pin high until a new read is
# requested, then move to 'SPI_TX' to send the read command.
# Also keep 'ack' asserted until 'stb' is released.
with m.State( "SPI_WAITING" ):
m.d.sync += [
self.arb.bus.ack.eq( self.arb.bus.cyc &
( self.arb.bus.ack & self.arb.bus.stb ) ),
self.spi.cs.o.eq( 0 )
]
m.next = "SPI_WAITING"
with m.If( ( self.arb.bus.cyc == 1 ) &
( self.arb.bus.stb == 1 ) &
( self.arb.bus.ack == 0 ) ):
m.d.sync += [
self.spi.cs.o.eq( 1 ),
self.spio.eq( ( 0x03000000 | ( ( self.arb.bus.adr + self.dstart ) & 0x00FFFFFF ) ) ),
self.arb.bus.ack.eq( 0 ),
self.dc.eq( 31 )
]
m.next = "SPI_TX"
# 'Send read command' state: transmits the 0x03 'read' command
# followed by the desired 24-bit address. (Encoded in 'spio')
with m.State( "SPI_TX" ):
# Set the 'mosi' pin to the next value and increment 'dc'.
m.d.sync += [
self.dc.eq( self.dc - 1 ),
self.spio.eq( self.spio << 1 )
]
m.d.comb += [
self.spi.clk.o.eq( ~ClockSignal( "sync" ) ),
self.spi.mosi.o.eq( self.spio[ 31 ] )
]
# Move to 'receive data' state once 32 bits have elapsed.
# Also clear 'dat_r' and 'dc' before doing so.
with m.If( self.dc == 0 ):
m.d.sync += [
self.dc.eq( 7 ),
self.arb.bus.dat_r.eq( 0 )
]
m.next = "SPI_RX"
with m.Else():
m.next = "SPI_TX"
# 'Receive data' state: continue the clock signal and read
# the 'miso' pin on rising edges.
# You can keep the clock signal going to receive as many bytes
# as you want, but this implementation only fetches one word.
with m.State( "SPI_RX" ):
# Simulate the 'miso' pin value for tests.
if platform is None:
with m.If( self.dc < 8 ):
m.d.comb += self.spi.miso.i.eq( ( self.data[ self.arb.bus.adr >> 2 ] >> ( self.dc + 24 ) ) & 0b1 )
with m.Elif( self.dc < 16 ):
m.d.comb += self.spi.miso.i.eq( ( self.data[ self.arb.bus.adr >> 2 ] >> ( self.dc + 8 ) ) & 0b1 )
with m.Elif( self.dc < 24 ):
m.d.comb += self.spi.miso.i.eq( ( self.data[ self.arb.bus.adr >> 2 ] >> ( self.dc - 8 ) ) & 0b1 )
with m.Else():
m.d.comb += self.spi.miso.i.eq( ( self.data[ self.arb.bus.adr >> 2 ] >> ( self.dc - 24 ) ) & 0b1 )
m.d.sync += [
self.dc.eq( self.dc - 1 ),
self.arb.bus.dat_r.bit_select( self.dc, 1 ).eq( self.spi.miso.i )
]
m.d.comb += self.spi.clk.o.eq( ~ClockSignal( "sync" ) )
# Assert 'ack' signal and move back to 'waiting' state
# once a whole word of data has been received.
with m.If( self.dc[ :3 ] == 0 ):
with m.If( self.dc[ 3 : 5 ] == 0b11 ):
m.d.sync += [
self.spi.cs.o.eq( 0 ),
self.arb.bus.ack.eq( self.arb.bus.cyc )
]
m.next = "SPI_WAITING"
with m.Else():
m.d.sync += self.dc.eq( self.dc + 15 )
m.next = "SPI_RX"
with m.Else():
m.next = "SPI_RX"
# (End of SPI Flash "ROM" module logic)
return m
##############################
# SPI Flash "ROM" testbench: #
##############################
# Keep track of test pass / fail rates.
p = 0
f = 0
# Helper method to record unit test pass/fails.
def spi_rom_ut( name, actual, expected ):
global p, f
if expected != actual:
f += 1
print( "\033[31mFAIL:\033[0m %s (0x%08X != 0x%08X)"
%( name, actual, expected ) )
else:
p += 1
print( "\033[32mPASS:\033[0m %s (0x%08X == 0x%08X)"
%( name, actual, expected ) )
# Helper method to test reading a byte of SPI data.
def spi_read_word( srom, virt_addr, phys_addr, simword, end_wait ):
# Set 'address'.
yield srom.arb.bus.adr.eq( virt_addr )
# Set 'strobe' and 'cycle' to request a new read.
yield srom.arb.bus.stb.eq( 1 )
yield srom.arb.bus.cyc.eq( 1 )
# Wait a tick; the (inverted) CS pin should then be low, and
# the 'read command' value should be set correctly.
yield Tick()
yield Settle()
csa = yield srom.spi.cs.o
spcmd = yield srom.spio
spi_rom_ut( "CS Low", csa, 1 )
spi_rom_ut( "SPI Read Cmd Value", spcmd, ( phys_addr & 0x00FFFFFF ) | 0x03000000 )
# Then the 32-bit read command is sent; two ticks per bit.
for i in range( 32 ):
yield Settle()
dout = yield srom.spi.mosi.o
spi_rom_ut( "SPI Read Cmd [%d]"%i, dout, ( spcmd >> ( 31 - i ) ) & 0b1 )
yield Tick()
# The following 32 bits should return the word. Simulate
# the requested word arriving on the MISO pin, MSbit first.
# (Data starts getting returned on the falling clock edge
# immediately following the last rising-edge read.)
i = 7
expect = 0
while i < 32:
yield Tick()
yield Settle()
expect = expect | ( ( 1 << i ) & simword )
progress = yield srom.arb.bus.dat_r
spi_rom_ut( "SPI Read Word [%d]"%i, progress, expect )
if ( ( i & 0b111 ) == 0 ):
i = i + 15
else:
i = i - 1
# Wait one more tick, then the CS signal should be de-asserted.
yield Tick()
yield Settle()
csa = yield srom.spi.cs.o
spi_rom_ut( "CS High (Waiting)", csa, 0 )
# Done; reset 'strobe' and 'cycle' after N ticks to test
# delayed reads from the bus.
for i in range( end_wait ):
yield Tick()
yield srom.arb.bus.stb.eq( 0 )
yield srom.arb.bus.cyc.eq( 0 )
yield Tick()
yield Settle()
# Top-level SPI ROM test method.
def spi_rom_tests( srom ):
global p, f
# Let signals settle after reset.
yield Tick()
yield Settle()
# Print a test header.
print( "--- SPI Flash 'ROM' Tests ---" )
# Test basic behavior by reading a few consecutive words.
yield from spi_read_word( srom, 0x00, 0x200000, LITTLE_END( 0x89ABCDEF ), 0 )
yield from spi_read_word( srom, 0x04, 0x200004, LITTLE_END( 0x0C0FFEE0 ), 4 )
# Make sure the CS pin stays de-asserted while waiting.
for i in range( 4 ):
yield Tick()
yield Settle()
csa = yield srom.spi.cs.o
spi_rom_ut( "CS High (Waiting)", csa, 0 )
yield from spi_read_word( srom, 0x10, 0x200010, LITTLE_END( 0xDEADFACE ), 1 )
yield from spi_read_word( srom, 0x0C, 0x20000C, LITTLE_END( 0xABACADAB ), 1 )
# Done. Print the number of passed and failed unit tests.
yield Tick()
print( "SPI 'ROM' Tests: %d Passed, %d Failed"%( p, f ) )
# 'main' method to run a basic testbench.
if __name__ == "__main__":
# Instantiate a test SPI ROM module.
off = ( 2 * 1024 * 1024 )
dut = SPI_ROM( off, off + 1024, [ 0x89ABCDEF, 0x0C0FFEE0, 0xBABABABA, 0xABACADAB, 0xDEADFACE, 0x12345678, 0x87654321, 0xDEADBEEF, 0xDEADBEEF ] )
# Run the SPI ROM tests.
with Simulator( dut, vcd_file = open( 'spi_rom.vcd', 'w' ) ) as sim:
def proc():
# Wait until the 'release power-down' command is sent.
# TODO: test that startup condition.
for i in range( 30 ):
yield Tick()
yield from spi_rom_tests( dut )
sim.add_clock( 1e-6 )
sim.add_sync_process( proc )
sim.run()