From e54fed1a75f7860f4661e99f1d55d6d370d81e5e Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 19 Aug 2025 15:27:16 +0100 Subject: [PATCH] SPI slave: add tx.py, rx.py, arx.py. Callback only runs in .read_into. --- rp2/RP2.md | 64 +++++++++++++++++++++++++++++++++++--------- rp2/spi/arx.py | 21 +++++++++++++++ rp2/spi/rx.py | 35 ++++++++++++++++++++++++ rp2/spi/spi_slave.py | 38 ++++++++++++++++++++------ rp2/spi/tx.py | 24 +++++++++++++++++ 5 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 rp2/spi/arx.py create mode 100644 rp2/spi/rx.py create mode 100644 rp2/spi/tx.py diff --git a/rp2/RP2.md b/rp2/RP2.md index dc98524..09be6c9 100644 --- a/rp2/RP2.md +++ b/rp2/RP2.md @@ -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 diff --git a/rp2/spi/arx.py b/rp2/spi/arx.py new file mode 100644 index 0000000..e7afa93 --- /dev/null +++ b/rp2/spi/arx.py @@ -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() diff --git a/rp2/spi/rx.py b/rp2/spi/rx.py new file mode 100644 index 0000000..7726a4c --- /dev/null +++ b/rp2/spi/rx.py @@ -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() diff --git a/rp2/spi/spi_slave.py b/rp2/spi/spi_slave.py index 869c290..48d9a4b 100644 --- a/rp2/spi/spi_slave.py +++ b/rp2/spi/spi_slave.py @@ -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 diff --git a/rp2/spi/tx.py b/rp2/spi/tx.py new file mode 100644 index 0000000..be79824 --- /dev/null +++ b/rp2/spi/tx.py @@ -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.")