1+ '''
2+
3+ FPGA breakout bitstream configurator.
4+
5+ Both PIO and SPI bit bang work to program Lattice FPGAs over the
6+ control and mux lines, as defined in the fabricfox FPGA breakout
7+ project
8+
9+ @author: Pat Deegan
10+ @copyright: Copyright (C) 2025 Pat Deegan, https://psychogenic.com
11+ '''
12+
13+ import rp2
14+ from machine import Pin
15+ from rp2 import PIO , StateMachine , asm_pio
16+ import utime
17+ from ttboard .pins .gpio_map import GPIOMap
18+ from ttboard .demoboard import DemoBoard
19+ DoDummyClocks = True
20+ def pin_indices ():
21+ '''
22+ returns map of pin ID (GPIO #)
23+ '''
24+ return {
25+ 'sck' : GPIOMap .MNG03 , # mng03,
26+ 'mosi' : GPIOMap .MNG00 , # mng 00,
27+ 'ss' : GPIOMap .MNG02 , # mng02,
28+ 'reset' : GPIOMap .ctrl_reset ()
29+ }
30+
31+ def pin_objects (tt ):
32+ '''
33+ returns map of pin objects
34+ '''
35+ # tt.pins.mng00.init(Pin.OUT)
36+ # tt.pins.mng02.init(Pin.OUT)
37+ # tt.pins.mng03.init(Pin.OUT)
38+ return {
39+ 'sck' : tt .pins .mng03 ,
40+ 'mosi' : tt .pins .mng00 ,
41+ 'ss' : tt .pins .mng02 ,
42+ 'reset' :tt .pins .ncrst
43+ }
44+
45+
46+
47+ # Simple PIO to do writes over SPI
48+
49+ # PIO program for SPI write-only (8 bits, SCK on Pin 1, MOSI on Pin 2)
50+ @rp2 .asm_pio (
51+ sideset_init = (rp2 .PIO .OUT_LOW ), # SCK idle low (CPOL=0)
52+ out_init = (rp2 .PIO .OUT_LOW ), # MOSI idle low
53+ out_shiftdir = rp2 .PIO .SHIFT_LEFT , #
54+ autopull = True , # Automatically pull 8 bits from FIFO
55+ pull_thresh = 8 , # Pull 8 bits at a time
56+ fifo_join = rp2 .PIO .JOIN_TX # Optimize for TX FIFO
57+ )
58+ def spi_write ():
59+ wrap_target ()
60+ set (x , 7 ) .side (0 ) # Initialize loop counter to 7 (for 8 bits), SCK low
61+ label ("bitloop" )
62+ out (pins , 1 ) .side (0 ) [1 ] # Output 1 bit to MOSI, SCK low, delay 1 cycle
63+ nop () .side (1 ) [1 ] # SCK high, delay 1 cycle (data sampled here)
64+ jmp (x_dec , "bitloop" ) .side (0 ) # Decrement X, loop if not zero, SCK low
65+ wrap ()
66+
67+
68+
69+
70+ def fpga_reset (ss , reset ):
71+ # xtra
72+ reset .low ()
73+ ss .low ()
74+ utime .sleep_us (15000 ) # Small delay to ensure reset
75+ reset .high ()
76+ utime .sleep_us (15000 ) # delay to ensure slave ready, minimum 1200us
77+
78+
79+
80+ def spi_transferPIO (filepath : str , freq : int = 1_000_000 ):
81+ """
82+ Transfer all bytes from a file over SPI using PIO.
83+
84+ Args:
85+ filepath (str): Path to the file to transmit.
86+ freq (int): SPI clock frequency in Hz (default 1 MHz).
87+ """
88+ tt = DemoBoard .get ()
89+ # Initialize pins
90+ pins = pin_objects (tt )
91+ pins_idx = pin_indices ()
92+
93+ reset = pins ['reset' ]
94+ ss = pins ['ss' ]
95+
96+ reset .high ()
97+
98+
99+ # Calculate PIO frequency (2 cycles per bit, 8 bits per byte)
100+ pio_freq = freq * 2 * 8 # e.g., 1 MHz SPI -> 16 MHz PIO
101+ print (f"Configuring PIO with frequency: { pio_freq } Hz" )
102+
103+ # Configure PIO state machine
104+ sm = StateMachine (0 , spi_write , freq = pio_freq , sideset_base = Pin (pins_idx ['sck' ]),
105+ out_base = Pin (pins_idx ['mosi' ]))
106+
107+ # Clear FIFO and ensure state machine is reset
108+ sm .restart ()
109+ sm .active (1 ) # Activate state machine
110+ print ("State machine activated" )
111+
112+ try :
113+ # Open file in binary read mode
114+ with open (filepath , 'rb' ) as f :
115+ # Set SS low to start SPI transaction
116+ fpga_reset (ss , reset )
117+
118+
119+ if DoDummyClocks :
120+ # release CS
121+ ss .high ()
122+ utime .sleep_us (2000 )
123+ # send 8 dummy clocks
124+ sm .put (0 )
125+
126+ # wait until sent
127+ utime .sleep_us (20 )
128+ while sm .tx_fifo () != 0 :
129+ utime .sleep_us (2 )
130+
131+
132+ # actually select
133+ ss .low ()
134+ utime .sleep_us (2000 )
135+
136+
137+ print ("SS low, starting transmission" )
138+
139+ # Read file in chunks
140+ chunk_size = 128 # Reduced chunk size to avoid FIFO overflow
141+ byte_count = 0
142+ termination_bytes_to_send = 6
143+ while True :
144+ data = f .read (chunk_size )
145+ if not data : # End of file
146+ for _i in range (termination_bytes_to_send ):
147+
148+ while sm .tx_fifo () != 0 :
149+ utime .sleep_us (1 )
150+
151+ sm .put (0 )
152+ break
153+
154+ # print(f"Sending {len(data)} bytes")
155+ # Send each byte to PIO TX FIFO
156+ for byte in data :
157+ # Wait if FIFO is full (tx_fifo() returns free slots, 0 means full)
158+
159+ sm .put (byte & 0xff , 24 ) # LEFT shift of the 32 bits, must push up 24 to see byte
160+ while sm .tx_fifo () != 0 :
161+ utime .sleep_us (1 ) # Small delay to prevent tight loop
162+ byte_count += 1
163+ #if byte_count % 128 == 0:
164+ # print(f"Sent {byte_count} bytes, TX FIFO free slots: {sm.tx_fifo()}")
165+
166+ # Wait for FIFO to drain
167+ while sm .tx_fifo ():
168+ print (f"Waiting for FIFO to drain, free slots: { sm .tx_fifo ()} " )
169+ utime .sleep_us (10 )
170+
171+ print (f"Transmission complete, total bytes: { byte_count } " )
172+
173+ except OSError as e :
174+ print (f"Error accessing file: { e } " )
175+ finally :
176+ # Deactivate state machine and set SS high
177+ sm .active (0 )
178+ ss .high ()
179+ sm .restart ()
180+ sm = None
181+ # sck.init(mode=Pin.OUT, pull=None)
182+ # mosi.init(mode=Pin.OUT)
183+
184+
185+ print ("State machine deactivated, SS high" )
186+
187+
188+
189+
190+
191+
192+ def spi_send (sck , mosi , val , delay_us = 1 ):
193+ sck .low ()
194+ for i in range (8 ):
195+ if val & (1 << (7 - i )):
196+ mosi .high ()
197+ else :
198+ mosi .low ()
199+ sck .high ()
200+ utime .sleep_us (delay_us )
201+ sck .low ()
202+ utime .sleep_us (delay_us )
203+
204+
205+ def spi_transferBitBang (filepath : str , freq : int = 1_000_000 ):
206+ """
207+ Transfer all bytes from a file over SPI using PIO.
208+
209+ Args:
210+ filepath (str): Path to the file to transmit.
211+ freq (int): SPI clock frequency in Hz (default 1 MHz).
212+ """
213+ # Initialize pins
214+ tt = DemoBoard .get ()
215+ # Initialize pins
216+ pins = pin_objects (tt )
217+ # pins_idx = pin_indices()
218+
219+ reset = pins ['reset' ]
220+ ss = pins ['ss' ]
221+ sck = pins ['sck' ]
222+ mosi = pins ['mosi' ]
223+ reset .high ()
224+
225+
226+
227+ try :
228+ # Open file in binary read mode
229+ with open (filepath , 'rb' ) as f :
230+ # Set SS low to start SPI transaction
231+ fpga_reset (ss , reset )
232+
233+
234+ if DoDummyClocks :
235+ # release CS
236+ ss .high ()
237+ utime .sleep_us (2000 )
238+ # send 8 dummy clocks
239+ spi_send (sck , mosi , 0 )
240+
241+ # wait until sent
242+ utime .sleep_us (20 )
243+
244+
245+ # actually select
246+ ss .low ()
247+ utime .sleep_us (2000 )
248+
249+
250+ print ("SS low, starting transmission" )
251+
252+ # Read file in chunks
253+ chunk_size = 128 # Reduced chunk size to avoid FIFO overflow
254+ byte_count = 0
255+ termination_bytes_to_send = 6
256+ while True :
257+ data = f .read (chunk_size )
258+ if not data : # End of file
259+ for _i in range (termination_bytes_to_send ):
260+
261+ spi_send (sck , mosi , 0 )
262+ break
263+
264+ # print(f"Sending {len(data)} bytes")
265+ # Send each byte to PIO TX FIFO
266+ for byte in data :
267+ # Wait if FIFO is full (tx_fifo() returns free slots, 0 means full)
268+ spi_send (sck , mosi , byte )
269+ byte_count += 1
270+ print (f"Transmission complete, total bytes: { byte_count } " )
271+
272+ except OSError as e :
273+ print (f"Error accessing file: { e } " )
274+ finally :
275+ # Deactivate state machine and set SS high
276+ # sck.init(mode=Pin.OUT, pull=None)
277+ # mosi.init(mode=Pin.OUT)
278+ ss .high ()
279+
280+
281+ print ("State machine deactivated, SS high" )
282+
283+ return {
284+ 'ss' : ss ,
285+ 'sck' : sck ,
286+ 'reset' : reset ,
287+ 'mosi' : mosi ,
288+ }
289+
290+
291+ # Example usage
292+ if __name__ == "__main__" :
293+ # Example: Transfer contents of 'data.bin' at 1 MHz
294+ spi_transferPIO ("/bitstreams/data.bin" , freq = 1_000_000 )
0 commit comments