From f22acb644b75d929525f317d6ec1b9b93ae24246 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Sun, 14 Sep 2025 12:36:19 +0100 Subject: [PATCH] RP2: Bidirectional SPI release. --- rp2/RP2.md | 30 +++++++++++++++++++++++------- rp2/spi/master_test.py | 20 ++++++++++---------- rp2/spi/spi_slave.py | 11 +++++------ 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/rp2/RP2.md b/rp2/RP2.md index 53b42ac..64e9c80 100644 --- a/rp2/RP2.md +++ b/rp2/RP2.md @@ -81,10 +81,19 @@ acquired (MISO): * `ibuf` A `bytearray` for MISO data. If the quantity of data exceeds the length of the buffer it will be truncated. +Using MISO affects the maximum achievable baudrate. This is because the incoming +signal is delayed by two system clock periods by the input synchronisers (RP2040 +manual 3.5.6.3), plus up to one additional system clock period because the slave +and master are mutually asynchronous. The latency becomes even worse when using +the asynchronous slave because the slave's `sck` is further delayed relative to +the master's by the slave's input synchronisers. In testing even 10MHz was too +high a rate for reliable reception. + ## 1.3 Methods * `write(data : bytes)` arg a `bytes` or `bytearray` of data to transmit. Return -is rapid with transmission running in the background. Returns `None`. +is rapid, returns `None`. The data is queued for transmission. The oldest object +is transmitted when the master next initiates a transfer. * `deinit()` Disables the DMA and the SM. Returns `None`. ## 1.4 CS/ @@ -97,12 +106,14 @@ between power-up and application start-up. A value of a few KΩ is suggested. ## 1.5 Callback This runs when the DMA is complete. It takes no args and runs in a hard IRQ -context. Typical use is to set a `ThreadSafeFlag`, allowing a pending task to -resume. Typically this will deassert `CS/` and initiate processing of received -data. Note that the DMA completes before transmission ends due to bytes stored -in the SM FIFO. This is unlikely to have practical consequences because of -MicroPython latency: the master executes several MP instructions before the -callback runs, and the response to a `ThreadSafeFlag` typically takes >200μs. +context. It should deassert `CS/`. Typical use is to set a `ThreadSafeFlag`, +allowing a pending task to resume, to initiate processing of received data. + +Users of low baudrates (<1MHz) should note that the DMA completes before +transmission ends due to three bytes stored in the SM FIFO. This is unlikely to +have practical consequences because of MicroPython latency: where the callback +deasserts `CS/` the time between the last edge of `clk` and the trailing edge +of `CS/` was 93μs (RP2040 @125MHz). # 2. Nonblocking SPI slave @@ -252,6 +263,11 @@ The slave will ignore all interface activity until CS/ is driven low. It then receives data with the end of message identified by a low to high transition on CS/. +The input pins are very sensitive to electrical noise: wiring to the master must +be kept very short. This is because of the very high speed of the RP2 internal +logic (clock rates >=125MHz). Pulses of a few ns duration can cause the state +machine to respond, or can cause an IRQ to be raised. + # 3. Pulse Measurement The file `measure_pulse.py` is a simple demo of using the PIO to measure a pulse diff --git a/rp2/spi/master_test.py b/rp2/spi/master_test.py index 7f7e765..eb02f1a 100644 --- a/rp2/spi/master_test.py +++ b/rp2/spi/master_test.py @@ -3,15 +3,12 @@ # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2025 Peter Hinch -# Performs SPI output: check on scope or LA. -# TODO: -# Fails if baudrate >= 9MHz +# Performs SPI I/O. + +# Bidirectional test fails if baudrate >= 9MHz: see RP2.md for reason. + # Problem with pin nos? -# 11 Sept slave works bidirectionally with tx.py, but with this code slave receives -# nothing, but responds with correct data. Waveforms look identical. MOSI data looks correct. -# Slave gets correct data with unidirectional setup (no ibuf) -# 45us after last clock edge, clk emits a 250ns pulse. CS/ goes high 724us after last clock -# I think spurious clk is screwing RX SM. + from machine import Pin import asyncio from .spi_master import SpiMaster @@ -25,12 +22,15 @@ tsf = asyncio.ThreadSafeFlag() def callback(): # Hard ISR - pin_cs(1) # Decrease deassert time from 724us to 93us but still fails + pin_cs(1) # Decrease deassert time from 724us to 93us tsf.set() # Flag user code that transfer is complete buf = bytearray(100) +# Bidirectional test: spi = SpiMaster(6, 1_000_000, pin_sck, pin_mosi, callback, miso=pin_miso, ibuf=buf) +# For unidirectional test issue: +# spi = SpiMaster(6, 1_000_000, pin_sck, pin_mosi, callback)) async def send(data): @@ -42,7 +42,7 @@ async def send(data): async def main(): - src_data = bytearray(b"\xFF\x55\xAA\x00the quick brown fox jumps over the lazy dog") + src_data = bytearray(b"\x00 The quick brown fox jumps over the lazy dog") n = 0 while True: await send(src_data) diff --git a/rp2/spi/spi_slave.py b/rp2/spi/spi_slave.py index d2580d1..3348539 100644 --- a/rp2/spi/spi_slave.py +++ b/rp2/spi/spi_slave.py @@ -16,7 +16,7 @@ alloc_emergency_exception_buf(100) # 2 CS\ -@rp2.asm_pio(autopull=True) +@rp2.asm_pio(autopush=True, autopull=True) def spi_in(): label("escape") # Just started, transfer ended or overrun attempt. out(y, 32) # Get maximum byte count (blocking wait) @@ -37,14 +37,13 @@ def spi_in(): jmp("overrun") label("continue") jmp(x_dec, "bit") # Post decrement - push() + # push() wrap() # Next byte label("done") # ISR has sent data out(x, 32) # Discard it in_(y, 30) # Return amount of unfilled buffer truncated to 30 bits - push() - # TODO Is this valid given that push_thresh==8? # Truncation ensures that overrun returns a short int + # push() jmp("escape") @@ -95,7 +94,7 @@ class SpiSlave: sm_num, spi_in, in_shiftdir=rp2.PIO.SHIFT_LEFT, - # push_thresh=8, + push_thresh=8, in_base=mosi, jmp_pin=sck, ) @@ -187,7 +186,7 @@ class SpiSlave: return # ISR runs on trailing edge but SM is not running. Nothing to do. # See above comment re memfails on next line 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._nbytes = self._buflen - sp if sp != 0x07FF_FFFF else self._buflen self._dma.active(0) self._sm.active(0) self._tsf.set()