SPI slave: add tx.py, rx.py, arx.py. Callback only runs in .read_into.

master
Peter Hinch 2025-08-19 15:27:16 +01:00
rodzic d2a4fea5bf
commit e54fed1a75
5 zmienionych plików z 162 dodań i 20 usunięć

Wyświetl plik

@ -116,9 +116,9 @@ Tests are run by issuing (e.g.):
# 2.1 Introduction
This uses a PIO state machine (SM) with DMA to enable an RP2 to receive SPI
transfers from a host. Reception is non-blocking, enabling code to run while a
transfer is in progress. The principal application area is for fast transfer of
large messages.
transfers from a host. Non blocking reception is offered, enabling code to run
while awaiting a message and while a transfer is in progress. The principal
application area is for fast transfer of large messages.
# 2.2 SpiSlave class
@ -130,9 +130,11 @@ high, terminating the transfer.
This takes the following positional args:
* `buf=None` Optional `bytearray` for incoming data. This is required if using
the asynchronous iterator interface, otherwise it is unused.
* `callback=None` Optional callback for use with the synchronous API. It takes
a single arg, being the number of bytes received.
the asynchronous iterator interface or the blocking `.read()` method, otherwise
it is unused.
* `callback=None` Callback for use with the nonblocking synchronous API. It takes
a single arg, being the number of bytes received. It runs as a soft interrupt
service routine (ISR). The callback will only run in response to `.read_into()`.
* `sm_num=0` State machine number.
Keyword args: Pin instances, initialised as input. GPIO nos. must be consecutive
@ -143,13 +145,51 @@ starting from `mosi`.
# 2.4 Synchronous Interface
This is via `SpiSlave.read_into(buffer)` where `buffer` is a user supplied
`bytearray`. This must be large enough for the expected message. The method
returns immediately. When a message arrives and reception is complete, the
callback runs. Its integer arg is the number of bytes received.
Methods:
* `read()` Blocking read. Blocks until a message is received. Returns a
`memoryview` into the buffer passed to the constructor. This slice contains the
message. If a message is too long to fit the buffer excess bytes are lost.
* `read_into(buffer)` Nonblocking read into the passed buffer. This must be
large enough for the expected message. The method returns immediately. When a
message arrives and reception is complete, the callback runs. Its integer arg is
the number of bytes received. If a message is too long to fit the buffer, excess
bytes are lost.
If a message is too long to fit the buffer, when the buffer fills, subsequent
characters are lost.
The nonblocking `.read_into()` method enables processing to be done while
awaiting a complete message. The drawback (compared to `.read()`) is that
synchronisation is required. This is to ensure that, when a message is received,
the slave is set up to receive the next. The following illustrates this:
```python
from machine import Pin
from .spi_slave import SpiSlave
nbytes = 0 # Synchronisation
# Callback runs when transfer complete (soft ISR context)
def receive(num_bytes):
global nbytes
nbytes = num_bytes
mosi = Pin(0, Pin.IN) # Consecutive GPIO nos.
sck = Pin(1, Pin.IN)
csn = Pin(2, Pin.IN)
piospi = SpiSlave(callback=receive, sm_num=4, mosi=mosi, sck=sck, csn=csn)
def test():
global nbytes
buf = bytearray(300) # Read buffer
while True:
nbytes = 0
piospi.read_into(buf) # Initiate a read
while nbytes == 0: # Wait for message arrival
pass # Can do something useful while waiting
print(bytes(buf[:nbytes])) # Message received. Process it.
test()
```
Note that if the master sends a message before the slave has finished processing
its predecessor, data will be lost. This illustration is inefficient: allocation
free slicing could be achieved via a `memoryview` of `buf`.
# 2.5 Asynchronous Interface

21
rp2/spi/arx.py 100644
Wyświetl plik

@ -0,0 +1,21 @@
import asyncio
from machine import Pin, reset
from .spi_slave import SpiSlave
from time import sleep_ms
mosi = Pin(0, Pin.IN)
sck = Pin(1, Pin.IN)
csn = Pin(2, Pin.IN)
async def main():
piospi = SpiSlave(buf=bytearray(300), sm_num=0, mosi=mosi, sck=sck, csn=csn)
async for msg in piospi:
print(f"Received: {len(msg)} bytes:")
print(bytes(msg))
print()
try:
asyncio.run(main())
finally:
reset()

35
rp2/spi/rx.py 100644
Wyświetl plik

@ -0,0 +1,35 @@
from machine import Pin, reset
from .spi_slave import SpiSlave
from time import sleep_ms
mosi = Pin(0, Pin.IN)
sck = Pin(1, Pin.IN)
csn = Pin(2, Pin.IN)
nbytes = 0
buf = bytearray(300) # Read buffer
def receive(num_bytes):
global nbytes
nbytes = num_bytes
piospi = SpiSlave(callback=receive, sm_num=4, mosi=mosi, sck=sck, csn=csn)
def test():
global nbytes
while True:
nbytes = 0
piospi.read_into(buf) # Initiate a read
while nbytes == 0: # Wait for message arrival
pass # Can do something useful while waiting
print(bytes(buf[:nbytes])) # Message received. Process it.
try:
test()
finally:
reset()

Wyświetl plik

@ -52,6 +52,7 @@ class SpiSlave:
return 4 + d if rx else d
self._callback = callback
self._docb = False # By default CB des not run
# Set up DMA
self._dma = rp2.DMA()
# Transfer bytes, rather than words, don't increment the read address and pace the transfer.
@ -62,6 +63,7 @@ class SpiSlave:
if buf is not None:
self._mvb = memoryview(buf)
self._tsf = asyncio.ThreadSafeFlag()
self._read_done = False # Synchronisation for .read()
csn.irq(self._done, trigger=Pin.IRQ_RISING, hard=True) # IRQ occurs at end of transfer
self._sm = rp2.StateMachine(
sm_num,
@ -76,15 +78,32 @@ class SpiSlave:
return self
async def __anext__(self):
if self._buf is None:
raise OSError("No buffer provided to constructor.")
self.read_into(self._buf) # Initiate DMA and return.
self._bufcheck() # Ensure there is a valid buffer
self._rinto(self._buf) # Initiate DMA and return.
await self._tsf.wait() # Wait for CS/ high (master signals transfer complete)
return self._mvb[: self._nbytes]
# Initiate a read into a buffer. Immediate return.
def _bufcheck(self):
if self._buf is None:
raise OSError("No buffer provided to constructor.")
def read(self): # Blocking read, own buffer
self._bufcheck()
self._read_done = False
self._rinto(self._buf)
while not self._read_done:
pass
return self._buf[: self._nbytes]
# Initiate a nonblocking read into a buffer. Immediate return.
def read_into(self, buf):
if self._callback is not None:
self._docb = True
self._rinto(buf)
else:
raise ValueError("Missing callback function.")
def _rinto(self, buf): # .read_into() without callback
buflen = len(buf)
self._buflen = buflen # Save for ISR
self._dma.active(0) # De-activate befor re-configuring
@ -100,18 +119,21 @@ class SpiSlave:
def _done(self, _): # Get no. of bytes received.
self._dma.active(0)
self._sm.put(0) # Request no. of received bits
if not self._sm.rx_fifo(): # Occurs if .read_into() never called while CSN is low:
if not self._sm.rx_fifo(): # Occurs if ._rinto() never called while CSN is low:
# print("GH")
return # ISR runs on trailing edge but SM is not running. Nothing to do.
sp = self._sm.get() >> 3 # Bits->bytes: space left in buffer or 7ffffff on overflow
self._nbytes = self._buflen - sp if sp != 0x7FFFFFF else self._buflen
self._dma.active(0)
self._sm.active(0)
self._tsf.set()
if self._callback is not None:
self._read_done = True
if self._docb: # Only run CB if user has called .read_into()
self._docb = False
schedule(self._callback, self._nbytes) # Soft ISR
# Await a read into a user-supplied buffer. Return no. of bytes read.
async def as_read_into(self, buf):
self.read_into(buf) # Start the read
self._rinto(buf) # Start the read
await self._tsf.wait() # Wait for CS/ high (master signals transfer complete)
return self._nbytes

24
rp2/spi/tx.py 100644
Wyświetl plik

@ -0,0 +1,24 @@
from machine import Pin, SPI
from time import sleep_ms
cs = Pin(17, Pin.OUT, value=1) # Ensure CS/ is False before we try to receive.
pin_miso = Pin(16, Pin.IN) # Not used: keep driver happy
pin_sck = Pin(18, Pin.OUT, value=0)
pin_mosi = Pin(19, Pin.OUT, value=0)
spi = SPI(0, baudrate=10_000_000, sck=pin_sck, mosi=pin_mosi, miso=pin_miso)
def send(obuf):
cs(0)
spi.write(obuf)
# sleep_ms(10)
cs(1)
# print("sent", obuf)
sleep_ms(1000)
while True:
send("The quick brown fox")
send("jumps over the lazy dog.")